Path: blob/main/src/vs/workbench/browser/parts/views/treeView.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { DataTransfers, IDragAndDropData } from '../../../../base/browser/dnd.js';6import * as DOM from '../../../../base/browser/dom.js';7import * as cssJs from '../../../../base/browser/cssValue.js';8import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js';9import { ActionBar, IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js';10import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js';11import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js';12import { IIdentityProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';13import { ElementsDragAndDropData, ListViewTargetSector } from '../../../../base/browser/ui/list/listView.js';14import { IAsyncDataSource, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, ITreeNode, ITreeRenderer, TreeDragOverBubble } from '../../../../base/browser/ui/tree/tree.js';15import { CollapseAllAction } from '../../../../base/browser/ui/tree/treeDefaults.js';16import { ActionRunner, IAction, Separator } from '../../../../base/common/actions.js';17import { timeout } from '../../../../base/common/async.js';18import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';19import { Codicon } from '../../../../base/common/codicons.js';20import { isCancellationError } from '../../../../base/common/errors.js';21import { Emitter, Event } from '../../../../base/common/event.js';22import { createMatches, FuzzyScore } from '../../../../base/common/filters.js';23import { IMarkdownString, isMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';24import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';25import { Mimes } from '../../../../base/common/mime.js';26import { Schemas } from '../../../../base/common/network.js';27import { basename, dirname } from '../../../../base/common/resources.js';28import { isFalsyOrWhitespace } from '../../../../base/common/strings.js';29import { isString } from '../../../../base/common/types.js';30import { URI } from '../../../../base/common/uri.js';31import { generateUuid } from '../../../../base/common/uuid.js';32import './media/views.css';33import { VSDataTransfer } from '../../../../base/common/dataTransfer.js';34import { localize } from '../../../../nls.js';35import { createActionViewItem, getContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';36import { Action2, IMenuService, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';37import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js';38import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';39import { ContextKeyExpr, ContextKeyExpression, IContextKey, IContextKeyChangeEvent, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';40import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';41import { FileKind } from '../../../../platform/files/common/files.js';42import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';43import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';44import { ILabelService } from '../../../../platform/label/common/label.js';45import { WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js';46import { ILogService } from '../../../../platform/log/common/log.js';47import { INotificationService } from '../../../../platform/notification/common/notification.js';48import { IOpenerService } from '../../../../platform/opener/common/opener.js';49import { IProgressService } from '../../../../platform/progress/common/progress.js';50import { Registry } from '../../../../platform/registry/common/platform.js';51import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';52import { ColorScheme } from '../../../../platform/theme/common/theme.js';53import { FileThemeIcon, FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.js';54import { ThemeIcon } from '../../../../base/common/themables.js';55import { fillEditorsDragData } from '../../dnd.js';56import { IResourceLabel, ResourceLabels } from '../../labels.js';57import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from '../editor/editorCommands.js';58import { getLocationBasedViewColors, IViewPaneOptions, ViewPane } from './viewPane.js';59import { IViewletViewOptions } from './viewsViewlet.js';60import { Extensions, ITreeItem, ITreeItemLabel, ITreeView, ITreeViewDataProvider, ITreeViewDescriptor, ITreeViewDragAndDropController, IViewBadge, IViewDescriptorService, IViewsRegistry, ResolvableTreeItem, TreeCommand, TreeItemCollapsibleState, TreeViewItemHandleArg, TreeViewPaneHandleArg, ViewContainer, ViewContainerLocation } from '../../../common/views.js';61import { IActivityService, NumberBadge } from '../../../services/activity/common/activity.js';62import { IExtensionService } from '../../../services/extensions/common/extensions.js';63import { IHoverService, WorkbenchHoverDelegate } from '../../../../platform/hover/browser/hover.js';64import { CodeDataTransfers, LocalSelectionTransfer } from '../../../../platform/dnd/browser/dnd.js';65import { toExternalVSDataTransfer } from '../../../../editor/browser/dataTransfer.js';66import { CheckboxStateHandler, TreeItemCheckbox } from './checkbox.js';67import { setTimeout0 } from '../../../../base/common/platform.js';68import { AriaRole } from '../../../../base/browser/ui/aria/aria.js';69import { TelemetryTrustedValue } from '../../../../platform/telemetry/common/telemetryUtils.js';70import { ITreeViewsDnDService } from '../../../../editor/common/services/treeViewsDndService.js';71import { DraggedTreeItemsIdentifier } from '../../../../editor/common/services/treeViewsDnd.js';72import { IMarkdownRenderResult, MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';73import type { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js';74import { parseLinkedText } from '../../../../base/common/linkedText.js';75import { Button } from '../../../../base/browser/ui/button/button.js';76import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js';77import { IAccessibleViewInformationService } from '../../../services/accessibility/common/accessibleViewInformationService.js';78import { Command } from '../../../../editor/common/languages.js';7980export class TreeViewPane extends ViewPane {8182protected readonly treeView: ITreeView;83private _container: HTMLElement | undefined;84private _actionRunner: MultipleSelectionActionRunner;8586constructor(87options: IViewletViewOptions,88@IKeybindingService keybindingService: IKeybindingService,89@IContextMenuService contextMenuService: IContextMenuService,90@IConfigurationService configurationService: IConfigurationService,91@IContextKeyService contextKeyService: IContextKeyService,92@IViewDescriptorService viewDescriptorService: IViewDescriptorService,93@IInstantiationService instantiationService: IInstantiationService,94@IOpenerService openerService: IOpenerService,95@IThemeService themeService: IThemeService,96@INotificationService notificationService: INotificationService,97@IHoverService hoverService: IHoverService,98@IAccessibleViewInformationService accessibleViewService: IAccessibleViewInformationService,99) {100super({ ...(options as IViewPaneOptions), titleMenuId: MenuId.ViewTitle, donotForwardArgs: false }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService, accessibleViewService);101const { treeView } = (<ITreeViewDescriptor>Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).getView(options.id));102this.treeView = treeView;103this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this));104this._register(this.treeView.onDidChangeTitle((newTitle) => this.updateTitle(newTitle)));105this._register(this.treeView.onDidChangeDescription((newDescription) => this.updateTitleDescription(newDescription)));106this._register(toDisposable(() => {107if (this._container && this.treeView.container && (this._container === this.treeView.container)) {108this.treeView.setVisibility(false);109}110}));111this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility()));112this._register(this.treeView.onDidChangeWelcomeState(() => this._onDidChangeViewWelcomeState.fire()));113if (options.title !== this.treeView.title) {114this.updateTitle(this.treeView.title);115}116if (options.titleDescription !== this.treeView.description) {117this.updateTitleDescription(this.treeView.description);118}119this._actionRunner = this._register(new MultipleSelectionActionRunner(notificationService, () => this.treeView.getSelection()));120121this.updateTreeVisibility();122}123124override focus(): void {125super.focus();126this.treeView.focus();127}128129protected override renderBody(container: HTMLElement): void {130this._container = container;131super.renderBody(container);132this.renderTreeView(container);133}134135override shouldShowWelcome(): boolean {136return ((this.treeView.dataProvider === undefined) || !!this.treeView.dataProvider.isTreeEmpty) && ((this.treeView.message === undefined) || (this.treeView.message === ''));137}138139protected override layoutBody(height: number, width: number): void {140super.layoutBody(height, width);141this.layoutTreeView(height, width);142}143144override getOptimalWidth(): number {145return this.treeView.getOptimalWidth();146}147148protected renderTreeView(container: HTMLElement): void {149this.treeView.show(container);150}151152protected layoutTreeView(height: number, width: number): void {153this.treeView.layout(height, width);154}155156private updateTreeVisibility(): void {157this.treeView.setVisibility(this.isBodyVisible());158}159160override getActionRunner() {161return this._actionRunner;162}163164override getActionsContext(): TreeViewPaneHandleArg {165return { $treeViewId: this.id, $focusedTreeItem: true, $selectedTreeItems: true };166}167168}169170class Root implements ITreeItem {171label = { label: 'root' };172handle = '0';173parentHandle: string | undefined = undefined;174collapsibleState = TreeItemCollapsibleState.Expanded;175children: ITreeItem[] | undefined = undefined;176}177178function commandPreconditions(commandId: string): ContextKeyExpression | undefined {179const command = CommandsRegistry.getCommand(commandId);180if (command) {181const commandAction = MenuRegistry.getCommand(command.id);182return commandAction && commandAction.precondition;183}184return undefined;185}186187function isTreeCommandEnabled(treeCommand: TreeCommand | Command, contextKeyService: IContextKeyService): boolean {188const commandId: string = (treeCommand as TreeCommand).originalId ? (treeCommand as TreeCommand).originalId! : treeCommand.id;189const precondition = commandPreconditions(commandId);190if (precondition) {191return contextKeyService.contextMatchesRules(precondition);192}193194return true;195}196197interface RenderedMessage { element: HTMLElement; disposables: DisposableStore }198199function isRenderedMessageValue(messageValue: string | RenderedMessage | undefined): messageValue is RenderedMessage {200return !!messageValue && typeof messageValue !== 'string' && !!messageValue.element && !!messageValue.disposables;201}202203const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data.");204205export const RawCustomTreeViewContextKey = new RawContextKey<boolean>('customTreeView', false);206207class Tree extends WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> { }208209abstract class AbstractTreeView extends Disposable implements ITreeView {210211private isVisible: boolean = false;212private _hasIconForParentNode = false;213private _hasIconForLeafNode = false;214215private collapseAllContextKey: RawContextKey<boolean> | undefined;216private collapseAllContext: IContextKey<boolean> | undefined;217private collapseAllToggleContextKey: RawContextKey<boolean> | undefined;218private collapseAllToggleContext: IContextKey<boolean> | undefined;219private refreshContextKey: RawContextKey<boolean> | undefined;220private refreshContext: IContextKey<boolean> | undefined;221222private focused: boolean = false;223private domNode!: HTMLElement;224private treeContainer: HTMLElement | undefined;225private _messageValue: string | { element: HTMLElement; disposables: DisposableStore } | undefined;226private _canSelectMany: boolean = false;227private _manuallyManageCheckboxes: boolean = false;228private messageElement: HTMLElement | undefined;229private tree: Tree | undefined;230private treeLabels: ResourceLabels | undefined;231private treeViewDnd: CustomTreeViewDragAndDrop | undefined;232private _container: HTMLElement | undefined;233234private root: ITreeItem;235private markdownRenderer: MarkdownRenderer | undefined;236private elementsToRefresh: ITreeItem[] = [];237private lastSelection: readonly ITreeItem[] = [];238private lastActive: ITreeItem;239240private readonly _onDidExpandItem: Emitter<ITreeItem> = this._register(new Emitter<ITreeItem>());241get onDidExpandItem(): Event<ITreeItem> { return this._onDidExpandItem.event; }242243private readonly _onDidCollapseItem: Emitter<ITreeItem> = this._register(new Emitter<ITreeItem>());244get onDidCollapseItem(): Event<ITreeItem> { return this._onDidCollapseItem.event; }245246private _onDidChangeSelectionAndFocus: Emitter<{ selection: readonly ITreeItem[]; focus: ITreeItem }> = this._register(new Emitter<{ selection: readonly ITreeItem[]; focus: ITreeItem }>());247get onDidChangeSelectionAndFocus(): Event<{ selection: readonly ITreeItem[]; focus: ITreeItem }> { return this._onDidChangeSelectionAndFocus.event; }248249private readonly _onDidChangeVisibility: Emitter<boolean> = this._register(new Emitter<boolean>());250get onDidChangeVisibility(): Event<boolean> { return this._onDidChangeVisibility.event; }251252private readonly _onDidChangeActions: Emitter<void> = this._register(new Emitter<void>());253get onDidChangeActions(): Event<void> { return this._onDidChangeActions.event; }254255private readonly _onDidChangeWelcomeState: Emitter<void> = this._register(new Emitter<void>());256get onDidChangeWelcomeState(): Event<void> { return this._onDidChangeWelcomeState.event; }257258private readonly _onDidChangeTitle: Emitter<string> = this._register(new Emitter<string>());259get onDidChangeTitle(): Event<string> { return this._onDidChangeTitle.event; }260261private readonly _onDidChangeDescription: Emitter<string | undefined> = this._register(new Emitter<string | undefined>());262get onDidChangeDescription(): Event<string | undefined> { return this._onDidChangeDescription.event; }263264private readonly _onDidChangeCheckboxState: Emitter<readonly ITreeItem[]> = this._register(new Emitter<readonly ITreeItem[]>());265get onDidChangeCheckboxState(): Event<readonly ITreeItem[]> { return this._onDidChangeCheckboxState.event; }266267private readonly _onDidCompleteRefresh: Emitter<void> = this._register(new Emitter<void>());268269constructor(270readonly id: string,271private _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: IOpenerService286) {287super();288this.root = new Root();289this.lastActive = this.root;290// Try not to add anything that could be costly to this constructor. It gets called once per tree view291// during startup, and anything added here can affect performance.292}293294private _isInitialized: boolean = false;295private initialize() {296if (this._isInitialized) {297return;298}299this._isInitialized = true;300301// Remember when adding to this method that it isn't called until the view is visible, meaning that302// properties could be set and events could be fired before we're initialized and that this needs to be handled.303304this.contextKeyService.bufferChangeEvents(() => {305this.initializeShowCollapseAllAction();306this.initializeCollapseAllToggle();307this.initializeShowRefreshAction();308});309310this.treeViewDnd = this.instantiationService.createInstance(CustomTreeViewDragAndDrop, this.id);311if (this._dragAndDropController) {312this.treeViewDnd.controller = this._dragAndDropController;313}314315this._register(this.configurationService.onDidChangeConfiguration(e => {316if (e.affectsConfiguration('explorer.decorations')) {317this.doRefresh([this.root]); /** soft refresh **/318}319}));320this._register(this.viewDescriptorService.onDidChangeLocation(({ views, from, to }) => {321if (views.some(v => v.id === this.id)) {322this.tree?.updateOptions({ overrideStyles: getLocationBasedViewColors(this.viewLocation).listOverrideStyles });323}324}));325this.registerActions();326327this.create();328}329330get viewContainer(): ViewContainer {331return this.viewDescriptorService.getViewContainerByViewId(this.id)!;332}333334get viewLocation(): ViewContainerLocation {335return this.viewDescriptorService.getViewLocationById(this.id)!;336}337private _dragAndDropController: ITreeViewDragAndDropController | undefined;338get dragAndDropController(): ITreeViewDragAndDropController | undefined {339return this._dragAndDropController;340}341set dragAndDropController(dnd: ITreeViewDragAndDropController | undefined) {342this._dragAndDropController = dnd;343if (this.treeViewDnd) {344this.treeViewDnd.controller = dnd;345}346}347348private _dataProvider: ITreeViewDataProvider | undefined;349get dataProvider(): ITreeViewDataProvider | undefined {350return this._dataProvider;351}352353set dataProvider(dataProvider: ITreeViewDataProvider | undefined) {354if (dataProvider) {355if (this.visible) {356this.activate();357}358const self = this;359this._dataProvider = new class implements ITreeViewDataProvider {360private _isEmpty: boolean = true;361private _onDidChangeEmpty: Emitter<void> = new Emitter();362public onDidChangeEmpty: Event<void> = this._onDidChangeEmpty.event;363364get isTreeEmpty(): boolean {365return this._isEmpty;366}367368async getChildren(element?: ITreeItem): Promise<ITreeItem[] | undefined> {369const batches = await this.getChildrenBatch(element ? [element] : undefined);370return batches?.[0];371}372373private updateEmptyState(nodes: ITreeItem[], childrenGroups: ITreeItem[][]): void {374if ((nodes.length === 1) && (nodes[0] instanceof Root)) {375const oldEmpty = this._isEmpty;376this._isEmpty = (childrenGroups.length === 0) || (childrenGroups[0].length === 0);377if (oldEmpty !== this._isEmpty) {378this._onDidChangeEmpty.fire();379}380}381}382383private findCheckboxesUpdated(nodes: ITreeItem[], childrenGroups: ITreeItem[][]): ITreeItem[] {384if (childrenGroups.length === 0) {385return [];386}387const checkboxesUpdated: ITreeItem[] = [];388389for (let i = 0; i < nodes.length; i++) {390const node = nodes[i];391const children = childrenGroups[i];392for (const child of children) {393child.parent = node;394if (!self.manuallyManageCheckboxes && (node?.checkbox?.isChecked === true) && (child.checkbox?.isChecked === false)) {395child.checkbox.isChecked = true;396checkboxesUpdated.push(child);397}398}399}400return checkboxesUpdated;401}402403async getChildrenBatch(nodes?: ITreeItem[]): Promise<ITreeItem[][]> {404let childrenGroups: ITreeItem[][];405let checkboxesUpdated: ITreeItem[] = [];406if (nodes && nodes.every((node): node is Required<ITreeItem & { children: ITreeItem[] }> => !!node.children)) {407childrenGroups = nodes.map(node => node.children);408} else {409nodes = nodes ?? [self.root];410const batchedChildren = await (nodes.length === 1 && nodes[0] instanceof Root ? doGetChildrenOrBatch(dataProvider, undefined) : doGetChildrenOrBatch(dataProvider, nodes));411for (let i = 0; i < nodes.length; i++) {412const node = nodes[i];413node.children = batchedChildren ? batchedChildren[i] : undefined;414}415childrenGroups = batchedChildren ?? [];416checkboxesUpdated = this.findCheckboxesUpdated(nodes, childrenGroups);417}418419this.updateEmptyState(nodes, childrenGroups);420421if (checkboxesUpdated.length > 0) {422self._onDidChangeCheckboxState.fire(checkboxesUpdated);423}424return childrenGroups;425}426};427if (this._dataProvider.onDidChangeEmpty) {428this._register(this._dataProvider.onDidChangeEmpty(() => {429this.updateCollapseAllToggle();430this._onDidChangeWelcomeState.fire();431}));432}433this.updateMessage();434this.refresh();435} else {436this._dataProvider = undefined;437this.treeDisposables.clear();438this.activated = false;439this.updateMessage();440}441442this._onDidChangeWelcomeState.fire();443}444445private _message: string | IMarkdownString | undefined;446get message(): string | IMarkdownString | undefined {447return this._message;448}449450set message(message: string | IMarkdownString | undefined) {451this._message = message;452this.updateMessage();453this._onDidChangeWelcomeState.fire();454}455456get title(): string {457return this._title;458}459460set title(name: string) {461this._title = name;462if (this.tree) {463this.tree.ariaLabel = this._title;464}465this._onDidChangeTitle.fire(this._title);466}467468private _description: string | undefined;469get description(): string | undefined {470return this._description;471}472473set description(description: string | undefined) {474this._description = description;475this._onDidChangeDescription.fire(this._description);476}477478private _badge: IViewBadge | undefined;479private readonly _activity = this._register(new MutableDisposable<IDisposable>());480481get badge(): IViewBadge | undefined {482return this._badge;483}484485set badge(badge: IViewBadge | undefined) {486487if (this._badge?.value === badge?.value &&488this._badge?.tooltip === badge?.tooltip) {489return;490}491492this._badge = badge;493if (badge) {494const activity = {495badge: new NumberBadge(badge.value, () => badge.tooltip),496priority: 50497};498this._activity.value = this.activityService.showViewActivity(this.id, activity);499} else {500this._activity.clear();501}502}503504get canSelectMany(): boolean {505return this._canSelectMany;506}507508set canSelectMany(canSelectMany: boolean) {509const oldCanSelectMany = this._canSelectMany;510this._canSelectMany = canSelectMany;511if (this._canSelectMany !== oldCanSelectMany) {512this.tree?.updateOptions({ multipleSelectionSupport: this.canSelectMany });513}514}515516get manuallyManageCheckboxes(): boolean {517return this._manuallyManageCheckboxes;518}519520set manuallyManageCheckboxes(manuallyManageCheckboxes: boolean) {521this._manuallyManageCheckboxes = manuallyManageCheckboxes;522}523524get hasIconForParentNode(): boolean {525return this._hasIconForParentNode;526}527528get hasIconForLeafNode(): boolean {529return this._hasIconForLeafNode;530}531532get visible(): boolean {533return this.isVisible;534}535536private initializeShowCollapseAllAction(startingValue: boolean = false) {537if (!this.collapseAllContext) {538this.collapseAllContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableCollapseAll`, startingValue, localize('treeView.enableCollapseAll', "Whether the tree view with id {0} enables collapse all.", this.id));539this.collapseAllContext = this.collapseAllContextKey.bindTo(this.contextKeyService);540}541return true;542}543544get showCollapseAllAction(): boolean {545this.initializeShowCollapseAllAction();546return !!this.collapseAllContext?.get();547}548549set showCollapseAllAction(showCollapseAllAction: boolean) {550this.initializeShowCollapseAllAction(showCollapseAllAction);551this.collapseAllContext?.set(showCollapseAllAction);552}553554555private initializeShowRefreshAction(startingValue: boolean = false) {556if (!this.refreshContext) {557this.refreshContextKey = new RawContextKey<boolean>(`treeView.${this.id}.enableRefresh`, startingValue, localize('treeView.enableRefresh', "Whether the tree view with id {0} enables refresh.", this.id));558this.refreshContext = this.refreshContextKey.bindTo(this.contextKeyService);559}560}561562get showRefreshAction(): boolean {563this.initializeShowRefreshAction();564return !!this.refreshContext?.get();565}566567set showRefreshAction(showRefreshAction: boolean) {568this.initializeShowRefreshAction(showRefreshAction);569this.refreshContext?.set(showRefreshAction);570}571572private registerActions() {573const that = this;574this._register(registerAction2(class extends Action2 {575constructor() {576super({577id: `workbench.actions.treeView.${that.id}.refresh`,578title: localize('refresh', "Refresh"),579menu: {580id: MenuId.ViewTitle,581when: ContextKeyExpr.and(ContextKeyExpr.equals('view', that.id), that.refreshContextKey),582group: 'navigation',583order: Number.MAX_SAFE_INTEGER - 1,584},585icon: Codicon.refresh586});587}588async run(): Promise<void> {589return that.refresh();590}591}));592this._register(registerAction2(class extends Action2 {593constructor() {594super({595id: `workbench.actions.treeView.${that.id}.collapseAll`,596title: localize('collapseAll', "Collapse All"),597menu: {598id: MenuId.ViewTitle,599when: ContextKeyExpr.and(ContextKeyExpr.equals('view', that.id), that.collapseAllContextKey),600group: 'navigation',601order: Number.MAX_SAFE_INTEGER,602},603precondition: that.collapseAllToggleContextKey,604icon: Codicon.collapseAll605});606}607async run(): Promise<void> {608if (that.tree) {609return new CollapseAllAction<ITreeItem, ITreeItem, FuzzyScore>(that.tree, true).run();610}611}612}));613}614615setVisibility(isVisible: boolean): void {616// Throughout setVisibility we need to check if the tree view's data provider still exists.617// This can happen because the `getChildren` call to the extension can return618// after the tree has been disposed.619620this.initialize();621isVisible = !!isVisible;622if (this.isVisible === isVisible) {623return;624}625626this.isVisible = isVisible;627628if (this.tree) {629if (this.isVisible) {630DOM.show(this.tree.getHTMLElement());631} else {632DOM.hide(this.tree.getHTMLElement()); // make sure the tree goes out of the tabindex world by hiding it633}634635if (this.isVisible && this.elementsToRefresh.length && this.dataProvider) {636this.doRefresh(this.elementsToRefresh);637this.elementsToRefresh = [];638}639}640641setTimeout0(() => {642if (this.dataProvider) {643this._onDidChangeVisibility.fire(this.isVisible);644}645});646647if (this.visible) {648this.activate();649}650}651652protected activated: boolean = false;653protected abstract activate(): void;654655focus(reveal: boolean = true, revealItem?: ITreeItem): void {656if (this.tree && this.root.children && this.root.children.length > 0) {657// Make sure the current selected element is revealed658const element = revealItem ?? this.tree.getSelection()[0];659if (element && reveal) {660this.tree.reveal(element, 0.5);661}662663// Pass Focus to Viewer664this.tree.domFocus();665} else if (this.tree && this.treeContainer && !this.treeContainer.classList.contains('hide')) {666this.tree.domFocus();667} else {668this.domNode.focus();669}670}671672show(container: HTMLElement): void {673this._container = container;674DOM.append(container, this.domNode);675}676677private create() {678this.domNode = DOM.$('.tree-explorer-viewlet-tree-view');679this.messageElement = DOM.append(this.domNode, DOM.$('.message'));680this.updateMessage();681this.treeContainer = DOM.append(this.domNode, DOM.$('.customview-tree'));682this.treeContainer.classList.add('file-icon-themable-tree', 'show-file-icons');683const focusTracker = this._register(DOM.trackFocus(this.domNode));684this._register(focusTracker.onDidFocus(() => this.focused = true));685this._register(focusTracker.onDidBlur(() => this.focused = false));686}687688private readonly treeDisposables: DisposableStore = this._register(new DisposableStore());689protected createTree() {690this.treeDisposables.clear();691const actionViewItemProvider = createActionViewItem.bind(undefined, this.instantiationService);692const treeMenus = this.treeDisposables.add(this.instantiationService.createInstance(TreeMenus, this.id));693this.treeLabels = this.treeDisposables.add(this.instantiationService.createInstance(ResourceLabels, this));694const dataSource = this.instantiationService.createInstance(TreeDataSource, this, <T>(task: Promise<T>) => this.progressService.withProgress({ location: this.id }, () => task));695const aligner = this.treeDisposables.add(new Aligner(this.themeService));696const checkboxStateHandler = this.treeDisposables.add(new CheckboxStateHandler());697const renderer = this.treeDisposables.add(this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner, checkboxStateHandler, () => this.manuallyManageCheckboxes));698this.treeDisposables.add(renderer.onDidChangeCheckboxState(e => this._onDidChangeCheckboxState.fire(e)));699700const widgetAriaLabel = this._title;701702this.tree = this.treeDisposables.add(this.instantiationService.createInstance(Tree, this.id, this.treeContainer!, new TreeViewDelegate(), [renderer],703dataSource, {704identityProvider: new TreeViewIdentityProvider(),705accessibilityProvider: {706getAriaLabel(element: ITreeItem): string | null {707if (element.accessibilityInformation) {708return element.accessibilityInformation.label;709}710711if (isString(element.tooltip)) {712return element.tooltip;713} else {714if (element.resourceUri && !element.label) {715// The custom tree has no good information on what should be used for the aria label.716// Allow the tree widget's default aria label to be used.717return null;718}719let buildAriaLabel: string = '';720if (element.label) {721buildAriaLabel += element.label.label + ' ';722}723if (element.description) {724buildAriaLabel += element.description;725}726return buildAriaLabel;727}728},729getRole(element: ITreeItem): AriaRole | undefined {730return element.accessibilityInformation?.role ?? 'treeitem';731},732getWidgetAriaLabel(): string {733return widgetAriaLabel;734}735},736keyboardNavigationLabelProvider: {737getKeyboardNavigationLabel: (item: ITreeItem) => {738return item.label ? item.label.label : (item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined);739}740},741expandOnlyOnTwistieClick: (e: ITreeItem) => {742return !!e.command || !!e.checkbox || this.configurationService.getValue<'singleClick' | 'doubleClick'>('workbench.tree.expandMode') === 'doubleClick';743},744collapseByDefault: (e: ITreeItem): boolean => {745return e.collapsibleState !== TreeItemCollapsibleState.Expanded;746},747multipleSelectionSupport: this.canSelectMany,748dnd: this.treeViewDnd,749overrideStyles: getLocationBasedViewColors(this.viewLocation).listOverrideStyles750}) as WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore>);751752this.treeDisposables.add(renderer.onDidChangeMenuContext(e => e.forEach(e => this.tree?.rerender(e))));753754this.treeDisposables.add(this.tree);755treeMenus.setContextKeyService(this.tree.contextKeyService);756aligner.tree = this.tree;757const actionRunner = this.treeDisposables.add(new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection()));758renderer.actionRunner = actionRunner;759760this.tree.contextKeyService.createKey<boolean>(this.id, true);761const customTreeKey = RawCustomTreeViewContextKey.bindTo(this.tree.contextKeyService);762customTreeKey.set(true);763this.treeDisposables.add(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner)));764765this.treeDisposables.add(this.tree.onDidChangeSelection(e => {766this.lastSelection = e.elements;767this.lastActive = this.tree?.getFocus()[0] ?? this.lastActive;768this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive });769}));770this.treeDisposables.add(this.tree.onDidChangeFocus(e => {771if (e.elements.length && (e.elements[0] !== this.lastActive)) {772this.lastActive = e.elements[0];773this.lastSelection = this.tree?.getSelection() ?? this.lastSelection;774this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive });775}776}));777this.treeDisposables.add(this.tree.onDidChangeCollapseState(e => {778if (!e.node.element) {779return;780}781782const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element;783if (e.node.collapsed) {784this._onDidCollapseItem.fire(element);785} else {786this._onDidExpandItem.fire(element);787}788}));789this.tree.setInput(this.root).then(() => this.updateContentAreas());790791this.treeDisposables.add(this.tree.onDidOpen(async (e) => {792if (!e.browserEvent) {793return;794}795if (e.browserEvent.target && (e.browserEvent.target as HTMLElement).classList.contains(TreeItemCheckbox.checkboxClass)) {796return;797}798const selection = this.tree!.getSelection();799const command = await this.resolveCommand(selection.length === 1 ? selection[0] : undefined);800801if (command && isTreeCommandEnabled(command, this.contextKeyService)) {802let args = command.arguments || [];803if (command.id === API_OPEN_EDITOR_COMMAND_ID || command.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) {804// Some commands owned by us should receive the805// `IOpenEvent` as context to open properly806args = [...args, e];807}808809try {810await this.commandService.executeCommand(command.id, ...args);811} catch (err) {812this.notificationService.error(err);813}814}815}));816817this.treeDisposables.add(treeMenus.onDidChange((changed) => {818if (this.tree?.hasNode(changed)) {819this.tree?.rerender(changed);820}821}));822}823824private async resolveCommand(element: ITreeItem | undefined): Promise<TreeCommand | undefined> {825let command = element?.command;826if (element && !command) {827if ((element instanceof ResolvableTreeItem) && element.hasResolve) {828await element.resolve(CancellationToken.None);829command = element.command;830}831}832return command;833}834835836private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent<ITreeItem>, actionRunner: MultipleSelectionActionRunner): void {837this.hoverService.hideHover();838const node: ITreeItem | null = treeEvent.element;839if (node === null) {840return;841}842const event: UIEvent = treeEvent.browserEvent;843844event.preventDefault();845event.stopPropagation();846847this.tree!.setFocus([node]);848let selected = this.canSelectMany ? this.getSelection() : [];849if (!selected.find(item => item.handle === node.handle)) {850selected = [node];851}852853const actions = treeMenus.getResourceContextActions(selected);854if (!actions.length) {855return;856}857this.contextMenuService.showContextMenu({858getAnchor: () => treeEvent.anchor,859860getActions: () => actions,861862getActionViewItem: (action) => {863const keybinding = this.keybindingService.lookupKeybinding(action.id);864if (keybinding) {865return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() });866}867return undefined;868},869870onHide: (wasCancelled?: boolean) => {871if (wasCancelled) {872this.tree!.domFocus();873}874},875876getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle } satisfies TreeViewItemHandleArg),877878actionRunner879});880}881882protected updateMessage(): void {883if (this._message) {884this.showMessage(this._message);885} else if (!this.dataProvider) {886this.showMessage(noDataProviderMessage);887} else {888this.hideMessage();889}890this.updateContentAreas();891}892893private processMessage(message: IMarkdownString, disposables: DisposableStore): HTMLElement {894const lines = message.value.split('\n');895const result: (IMarkdownRenderResult | HTMLElement)[] = [];896let hasFoundButton = false;897for (const line of lines) {898const linkedText = parseLinkedText(line);899900if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') {901const node = linkedText.nodes[0];902const buttonContainer = document.createElement('div');903buttonContainer.classList.add('button-container');904const button = new Button(buttonContainer, { title: node.title, secondary: hasFoundButton, supportIcons: true, ...defaultButtonStyles });905button.label = node.label;906button.onDidClick(_ => {907this.openerService.open(node.href, { allowCommands: true });908}, null, disposables);909910const href = URI.parse(node.href);911if (href.scheme === Schemas.command) {912const preConditions = commandPreconditions(href.path);913if (preConditions) {914button.enabled = this.contextKeyService.contextMatchesRules(preConditions);915disposables.add(this.contextKeyService.onDidChangeContext(e => {916if (e.affectsSome(new Set(preConditions.keys()))) {917button.enabled = this.contextKeyService.contextMatchesRules(preConditions);918}919}));920}921}922923disposables.add(button);924hasFoundButton = true;925result.push(buttonContainer);926} else {927hasFoundButton = false;928const rendered = this.markdownRenderer!.render(new MarkdownString(line, { isTrusted: message.isTrusted, supportThemeIcons: message.supportThemeIcons, supportHtml: message.supportHtml }));929result.push(rendered.element);930disposables.add(rendered);931}932}933934const container = document.createElement('div');935container.classList.add('rendered-message');936for (const child of result) {937if (DOM.isHTMLElement(child)) {938container.appendChild(child);939} else {940container.appendChild(child.element);941}942}943return container;944}945946private showMessage(message: string | IMarkdownString): void {947if (isRenderedMessageValue(this._messageValue)) {948this._messageValue.disposables.dispose();949}950if (isMarkdownString(message) && !this.markdownRenderer) {951this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {});952}953if (isMarkdownString(message)) {954const disposables = new DisposableStore();955const renderedMessage = this.processMessage(message, disposables);956this._messageValue = { element: renderedMessage, disposables };957} else {958this._messageValue = message;959}960if (!this.messageElement) {961return;962}963this.messageElement.classList.remove('hide');964this.resetMessageElement();965if (typeof this._messageValue === 'string' && !isFalsyOrWhitespace(this._messageValue)) {966this.messageElement.textContent = this._messageValue;967} else if (isRenderedMessageValue(this._messageValue)) {968this.messageElement.appendChild(this._messageValue.element);969}970this.layout(this._height, this._width);971}972973private hideMessage(): void {974this.resetMessageElement();975this.messageElement?.classList.add('hide');976this.layout(this._height, this._width);977}978979private resetMessageElement(): void {980if (this.messageElement) {981DOM.clearNode(this.messageElement);982}983}984985private _height: number = 0;986private _width: number = 0;987layout(height: number, width: number) {988if (height && width && this.messageElement && this.treeContainer) {989this._height = height;990this._width = width;991const treeHeight = height - DOM.getTotalHeight(this.messageElement);992this.treeContainer.style.height = treeHeight + 'px';993this.tree?.layout(treeHeight, width);994}995}996997getOptimalWidth(): number {998if (this.tree) {999const parentNode = this.tree.getHTMLElement();1000const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a'));1001return DOM.getLargestChildWidth(parentNode, childNodes);1002}1003return 0;1004}10051006private updateCheckboxes(elements: readonly ITreeItem[]): ITreeItem[] {1007return setCascadingCheckboxUpdates(elements);1008}10091010async refresh(elements?: readonly ITreeItem[], checkboxes?: readonly ITreeItem[]): Promise<void> {1011if (this.dataProvider && this.tree) {1012if (this.refreshing) {1013await Event.toPromise(this._onDidCompleteRefresh.event);1014}1015if (!elements) {1016elements = [this.root];1017// remove all waiting elements to refresh if root is asked to refresh1018this.elementsToRefresh = [];1019}1020for (const element of elements) {1021element.children = undefined; // reset children1022}1023if (this.isVisible) {1024const affectedElements = this.updateCheckboxes(checkboxes ?? []);1025return this.doRefresh(elements.concat(affectedElements));1026} else {1027if (this.elementsToRefresh.length) {1028const seen: Set<string> = new Set<string>();1029this.elementsToRefresh.forEach(element => seen.add(element.handle));1030for (const element of elements) {1031if (!seen.has(element.handle)) {1032this.elementsToRefresh.push(element);1033}1034}1035} else {1036this.elementsToRefresh.push(...elements);1037}1038}1039}1040return undefined;1041}10421043async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise<void> {1044const tree = this.tree;1045if (!tree) {1046return;1047}1048try {1049itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];1050for (const element of itemOrItems) {1051await tree.expand(element, false);1052}1053} catch (e) {1054// The extension could have changed the tree during the reveal.1055// Because of that, we ignore errors.1056}1057}10581059isCollapsed(item: ITreeItem): boolean {1060return !!this.tree?.isCollapsed(item);1061}10621063setSelection(items: ITreeItem[]): void {1064this.tree?.setSelection(items);1065}10661067getSelection(): ITreeItem[] {1068return this.tree?.getSelection() ?? [];1069}10701071setFocus(item?: ITreeItem): void {1072if (this.tree) {1073if (item) {1074this.focus(true, item);1075this.tree.setFocus([item]);1076} else if (this.tree.getFocus().length === 0) {1077this.tree.setFocus([]);1078}1079}1080}10811082async reveal(item: ITreeItem): Promise<void> {1083if (this.tree) {1084return this.tree.reveal(item);1085}1086}10871088private refreshing: boolean = false;1089private async doRefresh(elements: readonly ITreeItem[]): Promise<void> {1090const tree = this.tree;1091if (tree && this.visible) {1092this.refreshing = true;1093const oldSelection = tree.getSelection();1094try {1095await Promise.all(elements.map(element => tree.updateChildren(element, true, true)));1096} catch (e) {1097// When multiple calls are made to refresh the tree in quick succession,1098// we can get a "Tree element not found" error. This is expected.1099// Ideally this is fixable, so log instead of ignoring so the error is preserved.1100this.logService.error(e);1101}1102const newSelection = tree.getSelection();1103if (oldSelection.length !== newSelection.length || oldSelection.some((value, index) => value.handle !== newSelection[index].handle)) {1104this.lastSelection = newSelection;1105this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive });1106}1107this.refreshing = false;1108this._onDidCompleteRefresh.fire();1109this.updateContentAreas();1110if (this.focused) {1111this.focus(false);1112}1113this.updateCollapseAllToggle();1114}1115}11161117private initializeCollapseAllToggle() {1118if (!this.collapseAllToggleContext) {1119this.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));1120this.collapseAllToggleContext = this.collapseAllToggleContextKey.bindTo(this.contextKeyService);1121}1122}11231124private updateCollapseAllToggle() {1125if (this.showCollapseAllAction) {1126this.initializeCollapseAllToggle();1127this.collapseAllToggleContext?.set(!!this.root.children && (this.root.children.length > 0) &&1128this.root.children.some(value => value.collapsibleState !== TreeItemCollapsibleState.None));1129}1130}11311132private updateContentAreas(): void {1133const isTreeEmpty = !this.root.children || this.root.children.length === 0;1134// Hide tree container only when there is a message and tree is empty and not refreshing1135if (this._messageValue && isTreeEmpty && !this.refreshing && this.treeContainer) {1136// If there's a dnd controller then hiding the tree prevents it from being dragged into.1137if (!this.dragAndDropController) {1138this.treeContainer.classList.add('hide');1139}1140this.domNode.setAttribute('tabindex', '0');1141} else if (this.treeContainer) {1142this.treeContainer.classList.remove('hide');1143if (this.domNode === DOM.getActiveElement()) {1144this.focus();1145}1146this.domNode.removeAttribute('tabindex');1147}1148}11491150get container(): HTMLElement | undefined {1151return this._container;1152}1153}11541155class TreeViewIdentityProvider implements IIdentityProvider<ITreeItem> {1156getId(element: ITreeItem): { toString(): string } {1157return element.handle;1158}1159}11601161class TreeViewDelegate implements IListVirtualDelegate<ITreeItem> {11621163getHeight(element: ITreeItem): number {1164return TreeRenderer.ITEM_HEIGHT;1165}11661167getTemplateId(element: ITreeItem): string {1168return TreeRenderer.TREE_TEMPLATE_ID;1169}1170}11711172async function doGetChildrenOrBatch(dataProvider: ITreeViewDataProvider, nodes: ITreeItem[] | undefined): Promise<ITreeItem[][] | undefined> {1173if (dataProvider.getChildrenBatch) {1174return dataProvider.getChildrenBatch(nodes);1175} else {1176if (nodes) {1177return Promise.all(nodes.map(node => dataProvider.getChildren(node).then(children => children ?? [])));1178} else {1179return [await dataProvider.getChildren()].filter(children => children !== undefined);1180}1181}1182}11831184class TreeDataSource implements IAsyncDataSource<ITreeItem, ITreeItem> {11851186constructor(1187private treeView: ITreeView,1188private withProgress: <T>(task: Promise<T>) => Promise<T>1189) {1190}11911192hasChildren(element: ITreeItem): boolean {1193return !!this.treeView.dataProvider && (element.collapsibleState !== TreeItemCollapsibleState.None);1194}11951196private batch: ITreeItem[] | undefined;1197private batchPromise: Promise<ITreeItem[][] | undefined> | undefined;1198async getChildren(element: ITreeItem): Promise<ITreeItem[]> {1199const dataProvider = this.treeView.dataProvider;1200if (!dataProvider) {1201return [];1202}1203if (this.batch === undefined) {1204this.batch = [element];1205this.batchPromise = undefined;1206} else {1207this.batch.push(element);1208}1209const indexInBatch = this.batch.length - 1;1210return new Promise<ITreeItem[]>((resolve, reject) => {1211setTimeout(async () => {1212const batch = this.batch;1213this.batch = undefined;1214if (!this.batchPromise) {1215this.batchPromise = this.withProgress(doGetChildrenOrBatch(dataProvider, batch));1216}1217try {1218const result = await this.batchPromise;1219resolve((result && (indexInBatch < result.length)) ? result[indexInBatch] : []);1220} catch (e) {1221if (!(<string>e.message).startsWith('Bad progress location:')) {1222reject(e);1223}1224}1225}, 0);1226});1227}1228}12291230interface ITreeExplorerTemplateData {1231readonly container: HTMLElement;1232readonly resourceLabel: IResourceLabel;1233readonly icon: HTMLElement;1234readonly checkboxContainer: HTMLElement;1235checkbox?: TreeItemCheckbox;1236readonly actionBar: ActionBar;1237}12381239class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyScore, ITreeExplorerTemplateData> {1240static readonly ITEM_HEIGHT = 22;1241static readonly TREE_TEMPLATE_ID = 'treeExplorer';12421243private readonly _onDidChangeCheckboxState: Emitter<readonly ITreeItem[]> = this._register(new Emitter<readonly ITreeItem[]>());1244readonly onDidChangeCheckboxState: Event<readonly ITreeItem[]> = this._onDidChangeCheckboxState.event;12451246private _onDidChangeMenuContext: Emitter<readonly ITreeItem[]> = this._register(new Emitter<readonly ITreeItem[]>());1247readonly onDidChangeMenuContext: Event<readonly ITreeItem[]> = this._onDidChangeMenuContext.event;12481249private _actionRunner: MultipleSelectionActionRunner | undefined;1250private _hoverDelegate: IHoverDelegate;1251private _hasCheckbox: boolean = false;1252private _renderedElements = new Map<string, { original: ITreeNode<ITreeItem, FuzzyScore>; rendered: ITreeExplorerTemplateData }[]>(); // tree item handle to template data12531254constructor(1255private treeViewId: string,1256private menus: TreeMenus,1257private labels: ResourceLabels,1258private actionViewItemProvider: IActionViewItemProvider,1259private aligner: Aligner,1260private checkboxStateHandler: CheckboxStateHandler,1261private readonly manuallyManageCheckboxes: () => boolean,1262@IThemeService private readonly themeService: IThemeService,1263@IConfigurationService private readonly configurationService: IConfigurationService,1264@ILabelService private readonly labelService: ILabelService,1265@IContextKeyService private readonly contextKeyService: IContextKeyService,1266@IHoverService private readonly hoverService: IHoverService,1267@IInstantiationService instantiationService: IInstantiationService,1268) {1269super();1270this._hoverDelegate = this._register(instantiationService.createInstance(WorkbenchHoverDelegate, 'mouse', undefined, {}));1271this._register(this.themeService.onDidFileIconThemeChange(() => this.rerender()));1272this._register(this.themeService.onDidColorThemeChange(() => this.rerender()));1273this._register(checkboxStateHandler.onDidChangeCheckboxState(items => {1274this.updateCheckboxes(items);1275}));1276this._register(this.contextKeyService.onDidChangeContext(e => this.onDidChangeContext(e)));1277}12781279get templateId(): string {1280return TreeRenderer.TREE_TEMPLATE_ID;1281}12821283set actionRunner(actionRunner: MultipleSelectionActionRunner) {1284this._actionRunner = actionRunner;1285}12861287renderTemplate(container: HTMLElement): ITreeExplorerTemplateData {1288container.classList.add('custom-view-tree-node-item');12891290const checkboxContainer = DOM.append(container, DOM.$(''));1291const resourceLabel = this.labels.create(container, { supportHighlights: true, hoverDelegate: this._hoverDelegate });1292const icon = DOM.prepend(resourceLabel.element, DOM.$('.custom-view-tree-node-item-icon'));1293const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions'));1294const actionBar = new ActionBar(actionsContainer, {1295actionViewItemProvider: this.actionViewItemProvider1296});12971298return { resourceLabel, icon, checkboxContainer, actionBar, container };1299}13001301private getHover(label: string | undefined, resource: URI | null, node: ITreeItem): string | IManagedHoverTooltipMarkdownString | undefined {1302if (!(node instanceof ResolvableTreeItem) || !node.hasResolve) {1303if (resource && !node.tooltip) {1304return undefined;1305} else if (node.tooltip === undefined) {1306return label;1307} else if (!isString(node.tooltip)) {1308return { markdown: node.tooltip, markdownNotSupportedFallback: resource ? undefined : renderAsPlaintext(node.tooltip) }; // Passing undefined as the fallback for a resource falls back to the old native hover1309} else if (node.tooltip !== '') {1310return node.tooltip;1311} else {1312return undefined;1313}1314}13151316return {1317markdown: typeof node.tooltip === 'string' ? node.tooltip :1318(token: CancellationToken): Promise<IMarkdownString | string | undefined> => {1319return new Promise<IMarkdownString | string | undefined>((resolve) => {1320node.resolve(token).then(() => resolve(node.tooltip));1321});1322},1323markdownNotSupportedFallback: resource ? undefined : (label ?? '') // Passing undefined as the fallback for a resource falls back to the old native hover1324};1325}13261327renderElement(element: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {1328const node = element.element;1329const resource = node.resourceUri ? URI.revive(node.resourceUri) : null;1330const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : (resource ? { label: basename(resource) } : undefined);1331const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : undefined;1332const label = treeItemLabel ? treeItemLabel.label : undefined;1333const matches = (treeItemLabel && treeItemLabel.highlights && label) ? treeItemLabel.highlights.map(([start, end]) => {1334if (start < 0) {1335start = label.length + start;1336}1337if (end < 0) {1338end = label.length + end;1339}1340if ((start >= label.length) || (end > label.length)) {1341return ({ start: 0, end: 0 });1342}1343if (start > end) {1344const swap = start;1345start = end;1346end = swap;1347}1348return ({ start, end });1349}) : undefined;1350const icon = this.themeService.getColorTheme().type === ColorScheme.LIGHT ? node.icon : node.iconDark;1351const iconUrl = icon ? URI.revive(icon) : undefined;1352const title = this.getHover(label, resource, node);13531354// reset1355templateData.actionBar.clear();1356templateData.icon.style.color = '';13571358let commandEnabled = true;1359if (node.command) {1360commandEnabled = isTreeCommandEnabled(node.command, this.contextKeyService);1361}13621363this.renderCheckbox(node, templateData);13641365if (resource) {1366const fileDecorations = this.configurationService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations');1367const labelResource = resource ? resource : URI.parse('missing:_icon_resource');1368templateData.resourceLabel.setResource({ name: label, description, resource: labelResource }, {1369fileKind: this.getFileKind(node),1370title,1371hideIcon: this.shouldHideResourceLabelIcon(iconUrl, node.themeIcon),1372fileDecorations,1373extraClasses: ['custom-view-tree-node-item-resourceLabel'],1374matches: matches ? matches : createMatches(element.filterData),1375strikethrough: treeItemLabel?.strikethrough,1376disabledCommand: !commandEnabled,1377labelEscapeNewLines: true,1378forceLabel: !!node.label1379});1380} else {1381templateData.resourceLabel.setResource({ name: label, description }, {1382title,1383hideIcon: true,1384extraClasses: ['custom-view-tree-node-item-resourceLabel'],1385matches: matches ? matches : createMatches(element.filterData),1386strikethrough: treeItemLabel?.strikethrough,1387disabledCommand: !commandEnabled,1388labelEscapeNewLines: true1389});1390}13911392if (iconUrl) {1393templateData.icon.className = 'custom-view-tree-node-item-icon';1394templateData.icon.style.backgroundImage = cssJs.asCSSUrl(iconUrl);1395} else {1396let iconClass: string | undefined;1397if (this.shouldShowThemeIcon(!!resource, node.themeIcon)) {1398iconClass = ThemeIcon.asClassName(node.themeIcon);1399if (node.themeIcon.color) {1400templateData.icon.style.color = this.themeService.getColorTheme().getColor(node.themeIcon.color.id)?.toString() ?? '';1401}1402}1403templateData.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : '';1404templateData.icon.style.backgroundImage = '';1405}14061407if (!commandEnabled) {1408templateData.icon.className = templateData.icon.className + ' disabled';1409if (templateData.container.parentElement) {1410templateData.container.parentElement.className = templateData.container.parentElement.className + ' disabled';1411}1412}14131414templateData.actionBar.context = { $treeViewId: this.treeViewId, $treeItemHandle: node.handle } satisfies TreeViewItemHandleArg;14151416const menuActions = this.menus.getResourceActions([node]);1417templateData.actionBar.push(menuActions, { icon: true, label: false });14181419if (this._actionRunner) {1420templateData.actionBar.actionRunner = this._actionRunner;1421}1422this.setAlignment(templateData.container, node);14231424// remember rendered element, an element can be rendered multiple times1425const renderedItems = this._renderedElements.get(element.element.handle) ?? [];1426this._renderedElements.set(element.element.handle, [...renderedItems, { original: element, rendered: templateData }]);1427}14281429private rerender() {1430// As we add items to the map during this call we can't directly use the map in the for loop1431// but have to create a copy of the keys first1432const keys = new Set(this._renderedElements.keys());1433for (const key of keys) {1434const values = this._renderedElements.get(key) ?? [];1435for (const value of values) {1436this.disposeElement(value.original, 0, value.rendered);1437this.renderElement(value.original, 0, value.rendered);1438}1439}1440}14411442private renderCheckbox(node: ITreeItem, templateData: ITreeExplorerTemplateData) {1443if (node.checkbox) {1444// The first time we find a checkbox we want to rerender the visible tree to adapt the alignment1445if (!this._hasCheckbox) {1446this._hasCheckbox = true;1447this.rerender();1448}1449if (!templateData.checkbox) {1450const checkbox = new TreeItemCheckbox(templateData.checkboxContainer, this.checkboxStateHandler, this._hoverDelegate, this.hoverService);1451templateData.checkbox = checkbox;1452}1453templateData.checkbox.render(node);1454} else if (templateData.checkbox) {1455templateData.checkbox.dispose();1456templateData.checkbox = undefined;1457}1458}14591460private setAlignment(container: HTMLElement, treeItem: ITreeItem) {1461container.parentElement!.classList.toggle('align-icon-with-twisty', !this._hasCheckbox && this.aligner.alignIconWithTwisty(treeItem));1462}14631464private shouldHideResourceLabelIcon(iconUrl: URI | undefined, icon: ThemeIcon | undefined): boolean {1465// We always hide the resource label in favor of the iconUrl when it's provided.1466// When `ThemeIcon` is provided, we hide the resource label icon in favor of it only if it's a not a file icon.1467return (!!iconUrl || (!!icon && !this.isFileKindThemeIcon(icon)));1468}14691470private shouldShowThemeIcon(hasResource: boolean, icon: ThemeIcon | undefined): icon is ThemeIcon {1471if (!icon) {1472return false;1473}14741475// If there's a resource and the icon is a file icon, then the icon (or lack thereof) will already be coming from the1476// icon theme and should use whatever the icon theme has provided.1477return !(hasResource && this.isFileKindThemeIcon(icon));1478}14791480private isFolderThemeIcon(icon: ThemeIcon | undefined): boolean {1481return icon?.id === FolderThemeIcon.id;1482}14831484private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean {1485if (icon) {1486return icon.id === FileThemeIcon.id || this.isFolderThemeIcon(icon);1487} else {1488return false;1489}1490}14911492private getFileKind(node: ITreeItem): FileKind {1493if (node.themeIcon) {1494switch (node.themeIcon.id) {1495case FileThemeIcon.id:1496return FileKind.FILE;1497case FolderThemeIcon.id:1498return FileKind.FOLDER;1499}1500}1501return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE;1502}15031504private onDidChangeContext(e: IContextKeyChangeEvent) {1505const affectsEntireMenuContexts = e.affectsSome(this.menus.getEntireMenuContexts());15061507const items: ITreeItem[] = [];1508for (const [_, elements] of this._renderedElements) {1509for (const element of elements) {1510if (affectsEntireMenuContexts || e.affectsSome(this.menus.getElementOverlayContexts(element.original.element))) {1511items.push(element.original.element);1512}1513}1514}1515if (items.length) {1516this._onDidChangeMenuContext.fire(items);1517}1518}15191520private updateCheckboxes(items: ITreeItem[]) {1521let allItems: ITreeItem[] = [];15221523if (!this.manuallyManageCheckboxes()) {1524allItems = setCascadingCheckboxUpdates(items);1525} else {1526allItems = items;1527}15281529allItems.forEach(item => {1530const renderedItems = this._renderedElements.get(item.handle);1531if (renderedItems) {1532renderedItems.forEach(renderedItems => renderedItems.rendered.checkbox?.render(item));1533}1534});1535this._onDidChangeCheckboxState.fire(allItems);1536}15371538disposeElement(resource: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {1539const itemRenders = this._renderedElements.get(resource.element.handle) ?? [];1540const renderedIndex = itemRenders.findIndex(renderedItem => templateData === renderedItem.rendered);15411542if (itemRenders.length === 1) {1543this._renderedElements.delete(resource.element.handle);1544} else if (itemRenders.length > 0) {1545itemRenders.splice(renderedIndex, 1);1546}15471548templateData.checkbox?.dispose();1549templateData.checkbox = undefined;1550}15511552disposeTemplate(templateData: ITreeExplorerTemplateData): void {1553templateData.resourceLabel.dispose();1554templateData.actionBar.dispose();1555}1556}15571558class Aligner extends Disposable {1559private _tree: WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> | undefined;15601561constructor(private themeService: IThemeService) {1562super();1563}15641565set tree(tree: WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore>) {1566this._tree = tree;1567}15681569public alignIconWithTwisty(treeItem: ITreeItem): boolean {1570if (treeItem.collapsibleState !== TreeItemCollapsibleState.None) {1571return false;1572}1573if (!this.hasIcon(treeItem)) {1574return false;1575}15761577if (this._tree) {1578const parent: ITreeItem = this._tree.getParentElement(treeItem) || this._tree.getInput();1579if (this.hasIcon(parent)) {1580return !!parent.children && parent.children.some(c => c.collapsibleState !== TreeItemCollapsibleState.None && !this.hasIcon(c));1581}1582return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIcon(c));1583} else {1584return false;1585}1586}15871588private hasIcon(node: ITreeItem): boolean {1589const icon = this.themeService.getColorTheme().type === ColorScheme.LIGHT ? node.icon : node.iconDark;1590if (icon) {1591return true;1592}1593if (node.resourceUri || node.themeIcon) {1594const fileIconTheme = this.themeService.getFileIconTheme();1595const isFolder = node.themeIcon ? node.themeIcon.id === FolderThemeIcon.id : node.collapsibleState !== TreeItemCollapsibleState.None;1596if (isFolder) {1597return fileIconTheme.hasFileIcons && fileIconTheme.hasFolderIcons;1598}1599return fileIconTheme.hasFileIcons;1600}1601return false;1602}1603}16041605class MultipleSelectionActionRunner extends ActionRunner {16061607constructor(notificationService: INotificationService, private getSelectedResources: (() => ITreeItem[])) {1608super();1609this._register(this.onDidRun(e => {1610if (e.error && !isCancellationError(e.error)) {1611notificationService.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));1612}1613}));1614}16151616protected override async runAction(action: IAction, context: TreeViewItemHandleArg | TreeViewPaneHandleArg): Promise<void> {1617const selection = this.getSelectedResources();1618let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined;1619let actionInSelected: boolean = false;1620if (selection.length > 1) {1621selectionHandleArgs = selection.map(selected => {1622if ((selected.handle === (context as TreeViewItemHandleArg).$treeItemHandle) || (context as TreeViewPaneHandleArg).$selectedTreeItems) {1623actionInSelected = true;1624}1625return { $treeViewId: context.$treeViewId, $treeItemHandle: selected.handle };1626});1627}16281629if (!actionInSelected && selectionHandleArgs) {1630selectionHandleArgs = undefined;1631}16321633await action.run(context, selectionHandleArgs);1634}1635}16361637class TreeMenus implements IDisposable {1638private contextKeyService: IContextKeyService | undefined;1639private _onDidChange = new Emitter<ITreeItem>();1640public readonly onDidChange = this._onDidChange.event;16411642constructor(1643private id: string,1644@IMenuService private readonly menuService: IMenuService1645) { }16461647/**1648* Gets only the actions that apply to all of the given elements.1649*/1650getResourceActions(elements: ITreeItem[]): IAction[] {1651const actions = this.getActions(this.getMenuId(), elements);1652return actions.primary;1653}16541655/**1656* Gets only the actions that apply to all of the given elements.1657*/1658getResourceContextActions(elements: ITreeItem[]): IAction[] {1659return this.getActions(this.getMenuId(), elements).secondary;1660}16611662public setContextKeyService(service: IContextKeyService) {1663this.contextKeyService = service;1664}16651666private filterNonUniversalActions(groups: Map<string, IAction>[], newActions: IAction[]) {1667const newActionsSet: Set<string> = new Set(newActions.map(a => a.id));1668for (const group of groups) {1669const actions = group.keys();1670for (const action of actions) {1671if (!newActionsSet.has(action)) {1672group.delete(action);1673}1674}1675}1676}16771678private buildMenu(groups: Map<string, IAction>[]): IAction[] {1679const result: IAction[] = [];1680for (const group of groups) {1681if (group.size > 0) {1682if (result.length) {1683result.push(new Separator());1684}1685result.push(...group.values());1686}1687}1688return result;1689}16901691private createGroups(actions: IAction[]): Map<string, IAction>[] {1692const groups: Map<string, IAction>[] = [];1693let group: Map<string, IAction> = new Map();1694for (const action of actions) {1695if (action instanceof Separator) {1696groups.push(group);1697group = new Map();1698} else {1699group.set(action.id, action);1700}1701}1702groups.push(group);1703return groups;1704}17051706public getElementOverlayContexts(element: ITreeItem): Map<string, any> {1707return new Map([1708['view', this.id],1709['viewItem', element.contextValue]1710]);1711}17121713public getEntireMenuContexts(): ReadonlySet<string> {1714return this.menuService.getMenuContexts(this.getMenuId());1715}17161717public getMenuId(): MenuId {1718return MenuId.ViewItemContext;1719}17201721private getActions(menuId: MenuId, elements: ITreeItem[]): { primary: IAction[]; secondary: IAction[] } {1722if (!this.contextKeyService) {1723return { primary: [], secondary: [] };1724}17251726let primaryGroups: Map<string, IAction>[] = [];1727let secondaryGroups: Map<string, IAction>[] = [];1728for (let i = 0; i < elements.length; i++) {1729const element = elements[i];1730const contextKeyService = this.contextKeyService.createOverlay(this.getElementOverlayContexts(element));17311732const menuData = this.menuService.getMenuActions(menuId, contextKeyService, { shouldForwardArgs: true });17331734const result = getContextMenuActions(menuData, 'inline');1735if (i === 0) {1736primaryGroups = this.createGroups(result.primary);1737secondaryGroups = this.createGroups(result.secondary);1738} else {1739this.filterNonUniversalActions(primaryGroups, result.primary);1740this.filterNonUniversalActions(secondaryGroups, result.secondary);1741}1742}17431744return { primary: this.buildMenu(primaryGroups), secondary: this.buildMenu(secondaryGroups) };1745}17461747dispose() {1748this.contextKeyService = undefined;1749}1750}17511752export class CustomTreeView extends AbstractTreeView {17531754constructor(1755id: string,1756title: string,1757private readonly extensionId: string,1758@IThemeService themeService: IThemeService,1759@IInstantiationService instantiationService: IInstantiationService,1760@ICommandService commandService: ICommandService,1761@IConfigurationService configurationService: IConfigurationService,1762@IProgressService progressService: IProgressService,1763@IContextMenuService contextMenuService: IContextMenuService,1764@IKeybindingService keybindingService: IKeybindingService,1765@INotificationService notificationService: INotificationService,1766@IViewDescriptorService viewDescriptorService: IViewDescriptorService,1767@IContextKeyService contextKeyService: IContextKeyService,1768@IHoverService hoverService: IHoverService,1769@IExtensionService private readonly extensionService: IExtensionService,1770@IActivityService activityService: IActivityService,1771@ITelemetryService private readonly telemetryService: ITelemetryService,1772@ILogService logService: ILogService,1773@IOpenerService openerService: IOpenerService1774) {1775super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService, logService, openerService);1776}17771778protected activate() {1779if (!this.activated) {1780type ExtensionViewTelemetry = {1781extensionId: TelemetryTrustedValue<string>;1782id: string;1783};1784type ExtensionViewTelemetryMeta = {1785extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the extension' };1786id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the view' };1787owner: 'digitarald';1788comment: 'Helps to gain insights on what extension contributed views are most popular';1789};1790this.telemetryService.publicLog2<ExtensionViewTelemetry, ExtensionViewTelemetryMeta>('Extension:ViewActivate', {1791extensionId: new TelemetryTrustedValue(this.extensionId),1792id: this.id,1793});1794this.createTree();1795this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`))1796.then(() => timeout(2000))1797.then(() => {1798this.updateMessage();1799});1800this.activated = true;1801}1802}1803}18041805export class TreeView extends AbstractTreeView {18061807protected activate() {1808if (!this.activated) {1809this.createTree();1810this.activated = true;1811}1812}1813}18141815interface TreeDragSourceInfo {1816id: string;1817itemHandles: string[];1818}18191820export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> {1821private readonly treeMimeType: string;1822private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance<DraggedTreeItemsIdentifier>();1823private dragCancellationToken: CancellationTokenSource | undefined;18241825constructor(1826private readonly treeId: string,1827@ILabelService private readonly labelService: ILabelService,1828@IInstantiationService private readonly instantiationService: IInstantiationService,1829@ITreeViewsDnDService private readonly treeViewsDragAndDropService: ITreeViewsDnDService,1830@ILogService private readonly logService: ILogService) {1831this.treeMimeType = `application/vnd.code.tree.${treeId.toLowerCase()}`;1832}18331834private dndController: ITreeViewDragAndDropController | undefined;1835set controller(controller: ITreeViewDragAndDropController | undefined) {1836this.dndController = controller;1837}18381839private handleDragAndLog(dndController: ITreeViewDragAndDropController, itemHandles: string[], uuid: string, dragCancellationToken: CancellationToken): Promise<VSDataTransfer | undefined> {1840return dndController.handleDrag(itemHandles, uuid, dragCancellationToken).then(additionalDataTransfer => {1841if (additionalDataTransfer) {1842const unlistedTypes: string[] = [];1843for (const item of additionalDataTransfer) {1844if ((item[0] !== this.treeMimeType) && (dndController.dragMimeTypes.findIndex(value => value === item[0]) < 0)) {1845unlistedTypes.push(item[0]);1846}1847}1848if (unlistedTypes.length) {1849this.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(', ')}`);1850}1851}1852return additionalDataTransfer;1853});1854}18551856private addExtensionProvidedTransferTypes(originalEvent: DragEvent, itemHandles: string[]) {1857if (!originalEvent.dataTransfer || !this.dndController) {1858return;1859}1860const uuid = generateUuid();18611862this.dragCancellationToken = new CancellationTokenSource();1863this.treeViewsDragAndDropService.addDragOperationTransfer(uuid, this.handleDragAndLog(this.dndController, itemHandles, uuid, this.dragCancellationToken.token));1864this.treeItemsTransfer.setData([new DraggedTreeItemsIdentifier(uuid)], DraggedTreeItemsIdentifier.prototype);1865originalEvent.dataTransfer.clearData(Mimes.text);1866if (this.dndController.dragMimeTypes.find((element) => element === Mimes.uriList)) {1867// Add the type that the editor knows1868originalEvent.dataTransfer?.setData(DataTransfers.RESOURCES, '');1869}1870this.dndController.dragMimeTypes.forEach(supportedType => {1871originalEvent.dataTransfer?.setData(supportedType, '');1872});1873}18741875private addResourceInfoToTransfer(originalEvent: DragEvent, resources: URI[]) {1876if (resources.length && originalEvent.dataTransfer) {1877// Apply some datatransfer types to allow for dragging the element outside of the application1878this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, resources, originalEvent));18791880// The only custom data transfer we set from the explorer is a file transfer1881// to be able to DND between multiple code file explorers across windows1882const fileResources = resources.filter(s => s.scheme === Schemas.file).map(r => r.fsPath);1883if (fileResources.length) {1884originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));1885}1886}1887}18881889onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {1890if (originalEvent.dataTransfer) {1891const treeItemsData = (data as ElementsDragAndDropData<ITreeItem, ITreeItem[]>).getData();1892const resources: URI[] = [];1893const sourceInfo: TreeDragSourceInfo = {1894id: this.treeId,1895itemHandles: []1896};1897treeItemsData.forEach(item => {1898sourceInfo.itemHandles.push(item.handle);1899if (item.resourceUri) {1900resources.push(URI.revive(item.resourceUri));1901}1902});1903this.addResourceInfoToTransfer(originalEvent, resources);1904this.addExtensionProvidedTransferTypes(originalEvent, sourceInfo.itemHandles);1905originalEvent.dataTransfer.setData(this.treeMimeType,1906JSON.stringify(sourceInfo));1907}1908}19091910private debugLog(types: Set<string>) {1911if (types.size) {1912this.logService.debug(`TreeView dragged mime types: ${Array.from(types).join(', ')}`);1913} else {1914this.logService.debug(`TreeView dragged with no supported mime types.`);1915}1916}19171918onDragOver(data: IDragAndDropData, targetElement: ITreeItem, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {1919const dataTransfer = toExternalVSDataTransfer(originalEvent.dataTransfer!);19201921const types = new Set<string>(Array.from(dataTransfer, x => x[0]));19221923if (originalEvent.dataTransfer) {1924// Also add uri-list if we have any files. At this stage we can't actually access the file itself though.1925for (const item of originalEvent.dataTransfer.items) {1926if (item.kind === 'file' || item.type === DataTransfers.RESOURCES.toLowerCase()) {1927types.add(Mimes.uriList);1928break;1929}1930}1931}19321933this.debugLog(types);19341935const dndController = this.dndController;1936if (!dndController || !originalEvent.dataTransfer || (dndController.dropMimeTypes.length === 0)) {1937return false;1938}1939const dragContainersSupportedType = Array.from(types).some((value, index) => {1940if (value === this.treeMimeType) {1941return true;1942} else {1943return dndController.dropMimeTypes.indexOf(value) >= 0;1944}1945});1946if (dragContainersSupportedType) {1947return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: true };1948}1949return false;1950}19511952getDragURI(element: ITreeItem): string | null {1953if (!this.dndController) {1954return null;1955}1956return element.resourceUri ? URI.revive(element.resourceUri).toString() : element.handle;1957}19581959getDragLabel?(elements: ITreeItem[]): string | undefined {1960if (!this.dndController) {1961return undefined;1962}1963if (elements.length > 1) {1964return String(elements.length);1965}1966const element = elements[0];1967return element.label ? element.label.label : (element.resourceUri ? this.labelService.getUriLabel(URI.revive(element.resourceUri)) : undefined);1968}19691970async drop(data: IDragAndDropData, targetNode: ITreeItem | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): Promise<void> {1971const dndController = this.dndController;1972if (!originalEvent.dataTransfer || !dndController) {1973return;1974}19751976let treeSourceInfo: TreeDragSourceInfo | undefined;1977let willDropUuid: string | undefined;1978if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) {1979willDropUuid = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype)![0].identifier;1980}19811982const originalDataTransfer = toExternalVSDataTransfer(originalEvent.dataTransfer, true);19831984const outDataTransfer = new VSDataTransfer();1985for (const [type, item] of originalDataTransfer) {1986if (type === this.treeMimeType || dndController.dropMimeTypes.includes(type) || (item.asFile() && dndController.dropMimeTypes.includes(DataTransfers.FILES.toLowerCase()))) {1987outDataTransfer.append(type, item);1988if (type === this.treeMimeType) {1989try {1990treeSourceInfo = JSON.parse(await item.asString());1991} catch {1992// noop1993}1994}1995}1996}19971998const additionalDataTransfer = await this.treeViewsDragAndDropService.removeDragOperationTransfer(willDropUuid);1999if (additionalDataTransfer) {2000for (const [type, item] of additionalDataTransfer) {2001outDataTransfer.append(type, item);2002}2003}2004return dndController.handleDrop(outDataTransfer, targetNode, CancellationToken.None, willDropUuid, treeSourceInfo?.id, treeSourceInfo?.itemHandles);2005}20062007onDragEnd(originalEvent: DragEvent): void {2008// Check if the drag was cancelled.2009if (originalEvent.dataTransfer?.dropEffect === 'none') {2010this.dragCancellationToken?.cancel();2011}2012}20132014dispose(): void { }2015}20162017function setCascadingCheckboxUpdates(items: readonly ITreeItem[]) {2018const additionalItems: ITreeItem[] = [];20192020for (const item of items) {2021if (item.checkbox !== undefined) {20222023const checkChildren = (currentItem: ITreeItem) => {2024for (const child of (currentItem.children ?? [])) {2025if ((child.checkbox !== undefined) && (currentItem.checkbox !== undefined) && (child.checkbox.isChecked !== currentItem.checkbox.isChecked)) {2026child.checkbox.isChecked = currentItem.checkbox.isChecked;2027additionalItems.push(child);2028checkChildren(child);2029}2030}2031};2032checkChildren(item);20332034const visitedParents: Set<ITreeItem> = new Set();2035const checkParents = (currentItem: ITreeItem) => {2036if (currentItem.parent && (currentItem.parent.checkbox !== undefined) && currentItem.parent.children) {2037if (visitedParents.has(currentItem.parent)) {2038return;2039} else {2040visitedParents.add(currentItem.parent);2041}20422043let someUnchecked = false;2044let someChecked = false;2045for (const child of currentItem.parent.children) {2046if (someUnchecked && someChecked) {2047break;2048}2049if (child.checkbox !== undefined) {2050if (child.checkbox.isChecked) {2051someChecked = true;2052} else {2053someUnchecked = true;2054}2055}2056}2057if (someChecked && !someUnchecked && (currentItem.parent.checkbox.isChecked !== true)) {2058currentItem.parent.checkbox.isChecked = true;2059additionalItems.push(currentItem.parent);2060checkParents(currentItem.parent);2061} else if (someUnchecked && (currentItem.parent.checkbox.isChecked !== false)) {2062currentItem.parent.checkbox.isChecked = false;2063additionalItems.push(currentItem.parent);2064checkParents(currentItem.parent);2065}2066}2067};2068checkParents(item);2069}2070}20712072return items.concat(additionalItems);2073}207420752076