Path: blob/main/src/vs/workbench/browser/parts/views/treeView.ts
5260 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 { IRenderedMarkdown, 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 { isDark } 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 { IMarkdownRendererService } from '../../../../platform/markdown/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?.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 elementsToRefresh: ITreeItem[] = [];236private lastSelection: readonly ITreeItem[] = [];237private lastActive: ITreeItem;238239private readonly _onDidExpandItem: Emitter<ITreeItem> = this._register(new Emitter<ITreeItem>());240get onDidExpandItem(): Event<ITreeItem> { return this._onDidExpandItem.event; }241242private readonly _onDidCollapseItem: Emitter<ITreeItem> = this._register(new Emitter<ITreeItem>());243get onDidCollapseItem(): Event<ITreeItem> { return this._onDidCollapseItem.event; }244245private _onDidChangeSelectionAndFocus: Emitter<{ selection: readonly ITreeItem[]; focus: ITreeItem }> = this._register(new Emitter<{ selection: readonly ITreeItem[]; focus: ITreeItem }>());246get onDidChangeSelectionAndFocus(): Event<{ selection: readonly ITreeItem[]; focus: ITreeItem }> { return this._onDidChangeSelectionAndFocus.event; }247248private readonly _onDidChangeVisibility: Emitter<boolean> = this._register(new Emitter<boolean>());249get onDidChangeVisibility(): Event<boolean> { return this._onDidChangeVisibility.event; }250251private readonly _onDidChangeActions: Emitter<void> = this._register(new Emitter<void>());252get onDidChangeActions(): Event<void> { return this._onDidChangeActions.event; }253254private readonly _onDidChangeWelcomeState: Emitter<void> = this._register(new Emitter<void>());255get onDidChangeWelcomeState(): Event<void> { return this._onDidChangeWelcomeState.event; }256257private readonly _onDidChangeTitle: Emitter<string> = this._register(new Emitter<string>());258get onDidChangeTitle(): Event<string> { return this._onDidChangeTitle.event; }259260private readonly _onDidChangeDescription: Emitter<string | undefined> = this._register(new Emitter<string | undefined>());261get onDidChangeDescription(): Event<string | undefined> { return this._onDidChangeDescription.event; }262263private readonly _onDidChangeCheckboxState: Emitter<readonly ITreeItem[]> = this._register(new Emitter<readonly ITreeItem[]>());264get onDidChangeCheckboxState(): Event<readonly ITreeItem[]> { return this._onDidChangeCheckboxState.event; }265266private readonly _onDidCompleteRefresh: Emitter<void> = this._register(new Emitter<void>());267268constructor(269readonly id: string,270private _title: string,271@IThemeService private readonly themeService: IThemeService,272@IInstantiationService private readonly instantiationService: IInstantiationService,273@ICommandService private readonly commandService: ICommandService,274@IConfigurationService private readonly configurationService: IConfigurationService,275@IProgressService protected readonly progressService: IProgressService,276@IContextMenuService private readonly contextMenuService: IContextMenuService,277@IKeybindingService private readonly keybindingService: IKeybindingService,278@INotificationService private readonly notificationService: INotificationService,279@IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService,280@IHoverService private readonly hoverService: IHoverService,281@IContextKeyService private readonly contextKeyService: IContextKeyService,282@IActivityService private readonly activityService: IActivityService,283@ILogService private readonly logService: ILogService,284@IOpenerService private readonly openerService: IOpenerService,285@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,286) {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?.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, this.logService));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) {721const labelText = isMarkdownString(element.label.label) ? element.label.label.value : element.label.label;722buildAriaLabel += labelText + ' ';723}724if (element.description) {725buildAriaLabel += element.description;726}727return buildAriaLabel;728}729},730getRole(element: ITreeItem): AriaRole | undefined {731return element.accessibilityInformation?.role ?? 'treeitem';732},733getWidgetAriaLabel(): string {734return widgetAriaLabel;735}736},737keyboardNavigationLabelProvider: {738getKeyboardNavigationLabel: (item: ITreeItem) => {739if (item.label) {740return isMarkdownString(item.label.label) ? item.label.label.value : item.label.label;741}742return item.resourceUri ? basename(URI.revive(item.resourceUri)) : undefined;743}744},745expandOnlyOnTwistieClick: (e: ITreeItem) => {746return !!e.command || !!e.checkbox || this.configurationService.getValue<'singleClick' | 'doubleClick'>('workbench.tree.expandMode') === 'doubleClick';747},748collapseByDefault: (e: ITreeItem): boolean => {749return e.collapsibleState !== TreeItemCollapsibleState.Expanded;750},751multipleSelectionSupport: this.canSelectMany,752dnd: this.treeViewDnd,753overrideStyles: getLocationBasedViewColors(this.viewLocation).listOverrideStyles754}));755756this.treeDisposables.add(renderer.onDidChangeMenuContext(e => e.forEach(e => this.tree?.rerender(e))));757758this.treeDisposables.add(this.tree);759treeMenus.setContextKeyService(this.tree.contextKeyService);760aligner.tree = this.tree;761const actionRunner = this.treeDisposables.add(new MultipleSelectionActionRunner(this.notificationService, () => this.tree!.getSelection()));762renderer.actionRunner = actionRunner;763764this.tree.contextKeyService.createKey<boolean>(this.id, true);765const customTreeKey = RawCustomTreeViewContextKey.bindTo(this.tree.contextKeyService);766customTreeKey.set(true);767this.treeDisposables.add(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner)));768769this.treeDisposables.add(this.tree.onDidChangeSelection(e => {770this.lastSelection = e.elements;771this.lastActive = this.tree?.getFocus()[0] ?? this.lastActive;772this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive });773}));774this.treeDisposables.add(this.tree.onDidChangeFocus(e => {775if (e.elements.length && (e.elements[0] !== this.lastActive)) {776this.lastActive = e.elements[0];777this.lastSelection = this.tree?.getSelection() ?? this.lastSelection;778this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive });779}780}));781this.treeDisposables.add(this.tree.onDidChangeCollapseState(e => {782if (!e.node.element) {783return;784}785786const element: ITreeItem = Array.isArray(e.node.element.element) ? e.node.element.element[0] : e.node.element.element;787if (e.node.collapsed) {788this._onDidCollapseItem.fire(element);789} else {790this._onDidExpandItem.fire(element);791}792}));793this.tree.setInput(this.root).then(() => this.updateContentAreas());794795this.treeDisposables.add(this.tree.onDidOpen(async (e) => {796if (!e.browserEvent) {797return;798}799if (e.browserEvent.target && (e.browserEvent.target as HTMLElement).classList.contains(TreeItemCheckbox.checkboxClass)) {800return;801}802const selection = this.tree!.getSelection();803const command = await this.resolveCommand(selection.length === 1 ? selection[0] : undefined);804805if (command && isTreeCommandEnabled(command, this.contextKeyService)) {806let args = command.arguments || [];807if (command.id === API_OPEN_EDITOR_COMMAND_ID || command.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) {808// Some commands owned by us should receive the809// `IOpenEvent` as context to open properly810args = [...args, e];811}812813try {814await this.commandService.executeCommand(command.id, ...args);815} catch (err) {816this.notificationService.error(err);817}818}819}));820821this.treeDisposables.add(treeMenus.onDidChange((changed) => {822if (this.tree?.hasNode(changed)) {823this.tree?.rerender(changed);824}825}));826}827828private async resolveCommand(element: ITreeItem | undefined): Promise<TreeCommand | undefined> {829let command = element?.command;830if (element && !command) {831if ((element instanceof ResolvableTreeItem) && element.hasResolve) {832await element.resolve(CancellationToken.None);833command = element.command;834}835}836return command;837}838839840private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent<ITreeItem>, actionRunner: MultipleSelectionActionRunner): void {841this.hoverService.hideHover();842const node: ITreeItem | null = treeEvent.element;843if (node === null) {844return;845}846const event: UIEvent = treeEvent.browserEvent;847848event.preventDefault();849event.stopPropagation();850851this.tree!.setFocus([node]);852let selected = this.canSelectMany ? this.getSelection() : [];853if (!selected.find(item => item.handle === node.handle)) {854selected = [node];855}856857const actions = treeMenus.getResourceContextActions(selected);858if (!actions.length) {859return;860}861this.contextMenuService.showContextMenu({862getAnchor: () => treeEvent.anchor,863864getActions: () => actions,865866getActionViewItem: (action) => {867const keybinding = this.keybindingService.lookupKeybinding(action.id);868if (keybinding) {869return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel() });870}871return undefined;872},873874onHide: (wasCancelled?: boolean) => {875if (wasCancelled) {876this.tree!.domFocus();877}878},879880getActionsContext: () => ({ $treeViewId: this.id, $treeItemHandle: node.handle } satisfies TreeViewItemHandleArg),881882actionRunner883});884}885886protected updateMessage(): void {887if (this._message) {888this.showMessage(this._message);889} else if (!this.dataProvider) {890this.showMessage(noDataProviderMessage);891} else {892this.hideMessage();893}894this.updateContentAreas();895}896897private processMessage(message: IMarkdownString, disposables: DisposableStore): HTMLElement {898const lines = message.value.split('\n');899const result: (IRenderedMarkdown | HTMLElement)[] = [];900let hasFoundButton = false;901for (const line of lines) {902const linkedText = parseLinkedText(line);903904if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') {905const node = linkedText.nodes[0];906const buttonContainer = document.createElement('div');907buttonContainer.classList.add('button-container');908const button = new Button(buttonContainer, { title: node.title, secondary: hasFoundButton, supportIcons: true, ...defaultButtonStyles });909button.label = node.label;910button.onDidClick(_ => {911this.openerService.open(node.href, { allowCommands: true });912}, null, disposables);913914const href = URI.parse(node.href);915if (href.scheme === Schemas.command) {916const preConditions = commandPreconditions(href.path);917if (preConditions) {918button.enabled = this.contextKeyService.contextMatchesRules(preConditions);919disposables.add(this.contextKeyService.onDidChangeContext(e => {920if (e.affectsSome(new Set(preConditions.keys()))) {921button.enabled = this.contextKeyService.contextMatchesRules(preConditions);922}923}));924}925}926927disposables.add(button);928hasFoundButton = true;929result.push(buttonContainer);930} else {931hasFoundButton = false;932const rendered = this.markdownRendererService.render(new MarkdownString(line, { isTrusted: message.isTrusted, supportThemeIcons: message.supportThemeIcons, supportHtml: message.supportHtml }));933result.push(rendered.element);934disposables.add(rendered);935}936}937938const container = document.createElement('div');939container.classList.add('rendered-message');940for (const child of result) {941if (DOM.isHTMLElement(child)) {942container.appendChild(child);943} else {944container.appendChild(child.element);945}946}947return container;948}949950private showMessage(message: string | IMarkdownString): void {951if (isRenderedMessageValue(this._messageValue)) {952this._messageValue.disposables.dispose();953}954if (isMarkdownString(message)) {955const disposables = new DisposableStore();956const renderedMessage = this.processMessage(message, disposables);957this._messageValue = { element: renderedMessage, disposables };958} else {959this._messageValue = message;960}961if (!this.messageElement) {962return;963}964this.messageElement.classList.remove('hide');965this.resetMessageElement();966if (typeof this._messageValue === 'string' && !isFalsyOrWhitespace(this._messageValue)) {967this.messageElement.textContent = this._messageValue;968} else if (isRenderedMessageValue(this._messageValue)) {969this.messageElement.appendChild(this._messageValue.element);970}971this.layout(this._height, this._width);972}973974private hideMessage(): void {975this.resetMessageElement();976this.messageElement?.classList.add('hide');977this.layout(this._height, this._width);978}979980private resetMessageElement(): void {981if (this.messageElement) {982DOM.clearNode(this.messageElement);983}984}985986private _height: number = 0;987private _width: number = 0;988layout(height: number, width: number) {989if (height && width && this.messageElement && this.treeContainer) {990this._height = height;991this._width = width;992const treeHeight = height - DOM.getTotalHeight(this.messageElement);993this.treeContainer.style.height = treeHeight + 'px';994this.tree?.layout(treeHeight, width);995}996}997998getOptimalWidth(): number {999if (this.tree) {1000const parentNode = this.tree.getHTMLElement();1001// eslint-disable-next-line no-restricted-syntax1002const childNodes = ([] as HTMLElement[]).slice.call(parentNode.querySelectorAll('.outline-item-label > a'));1003return DOM.getLargestChildWidth(parentNode, childNodes);1004}1005return 0;1006}10071008private updateCheckboxes(elements: readonly ITreeItem[]): ITreeItem[] {1009return setCascadingCheckboxUpdates(elements);1010}10111012async refresh(elements?: readonly ITreeItem[], checkboxes?: readonly ITreeItem[]): Promise<void> {1013if (this.dataProvider && this.tree) {1014if (this.refreshing) {1015await Event.toPromise(this._onDidCompleteRefresh.event);1016}1017if (!elements) {1018elements = [this.root];1019// remove all waiting elements to refresh if root is asked to refresh1020this.elementsToRefresh = [];1021}1022for (const element of elements) {1023element.children = undefined; // reset children1024}1025if (this.isVisible) {1026const affectedElements = this.updateCheckboxes(checkboxes ?? []);1027return this.doRefresh(elements.concat(affectedElements));1028} else {1029if (this.elementsToRefresh.length) {1030const seen: Set<string> = new Set<string>();1031this.elementsToRefresh.forEach(element => seen.add(element.handle));1032for (const element of elements) {1033if (!seen.has(element.handle)) {1034this.elementsToRefresh.push(element);1035}1036}1037} else {1038this.elementsToRefresh.push(...elements);1039}1040}1041}1042return undefined;1043}10441045async expand(itemOrItems: ITreeItem | ITreeItem[]): Promise<void> {1046const tree = this.tree;1047if (!tree) {1048return;1049}1050try {1051itemOrItems = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];1052for (const element of itemOrItems) {1053await tree.expand(element, false);1054}1055} catch (e) {1056// The extension could have changed the tree during the reveal.1057// Because of that, we ignore errors.1058}1059}10601061isCollapsed(item: ITreeItem): boolean {1062return !!this.tree?.isCollapsed(item);1063}10641065setSelection(items: ITreeItem[]): void {1066this.tree?.setSelection(items);1067}10681069getSelection(): ITreeItem[] {1070return this.tree?.getSelection() ?? [];1071}10721073setFocus(item?: ITreeItem): void {1074if (this.tree) {1075if (item) {1076this.focus(true, item);1077this.tree.setFocus([item]);1078} else if (this.tree.getFocus().length === 0) {1079this.tree.setFocus([]);1080}1081}1082}10831084async reveal(item: ITreeItem): Promise<void> {1085if (this.tree) {1086return this.tree.reveal(item);1087}1088}10891090private refreshing: boolean = false;1091private async doRefresh(elements: readonly ITreeItem[]): Promise<void> {1092const tree = this.tree;1093if (tree && this.visible) {1094this.refreshing = true;1095const oldSelection = tree.getSelection();1096try {1097await Promise.all(elements.map(element => tree.updateChildren(element, true, true)));1098} catch (e) {1099// When multiple calls are made to refresh the tree in quick succession,1100// we can get a "Tree element not found" error. This is expected.1101// Ideally this is fixable, so log instead of ignoring so the error is preserved.1102this.logService.error(e);1103}1104const newSelection = tree.getSelection();1105if (oldSelection.length !== newSelection.length || oldSelection.some((value, index) => value.handle !== newSelection[index].handle)) {1106this.lastSelection = newSelection;1107this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive });1108}1109this.refreshing = false;1110this._onDidCompleteRefresh.fire();1111this.updateContentAreas();1112if (this.focused) {1113this.focus(false);1114}1115this.updateCollapseAllToggle();1116}1117}11181119private initializeCollapseAllToggle() {1120if (!this.collapseAllToggleContext) {1121this.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));1122this.collapseAllToggleContext = this.collapseAllToggleContextKey.bindTo(this.contextKeyService);1123}1124}11251126private updateCollapseAllToggle() {1127if (this.showCollapseAllAction) {1128this.initializeCollapseAllToggle();1129this.collapseAllToggleContext?.set(!!this.root.children && (this.root.children.length > 0) &&1130this.root.children.some(value => value.collapsibleState !== TreeItemCollapsibleState.None));1131}1132}11331134private updateContentAreas(): void {1135const isTreeEmpty = !this.root.children || this.root.children.length === 0;1136// Hide tree container only when there is a message and tree is empty and not refreshing1137if (this._messageValue && isTreeEmpty && !this.refreshing && this.treeContainer) {1138// If there's a dnd controller then hiding the tree prevents it from being dragged into.1139if (!this.dragAndDropController) {1140this.treeContainer.classList.add('hide');1141}1142this.domNode.setAttribute('tabindex', '0');1143} else if (this.treeContainer) {1144this.treeContainer.classList.remove('hide');1145if (this.domNode === DOM.getActiveElement()) {1146this.focus();1147}1148this.domNode.removeAttribute('tabindex');1149}1150}11511152get container(): HTMLElement | undefined {1153return this._container;1154}1155}11561157class TreeViewIdentityProvider implements IIdentityProvider<ITreeItem> {1158getId(element: ITreeItem): { toString(): string } {1159return element.handle;1160}1161}11621163class TreeViewDelegate implements IListVirtualDelegate<ITreeItem> {11641165getHeight(element: ITreeItem): number {1166return TreeRenderer.ITEM_HEIGHT;1167}11681169getTemplateId(element: ITreeItem): string {1170return TreeRenderer.TREE_TEMPLATE_ID;1171}1172}11731174async function doGetChildrenOrBatch(dataProvider: ITreeViewDataProvider, nodes: ITreeItem[] | undefined): Promise<ITreeItem[][] | undefined> {1175if (dataProvider.getChildrenBatch) {1176return dataProvider.getChildrenBatch(nodes);1177} else {1178if (nodes) {1179return Promise.all(nodes.map(node => dataProvider.getChildren(node).then(children => children ?? [])));1180} else {1181return [await dataProvider.getChildren()].filter(children => children !== undefined);1182}1183}1184}11851186class TreeDataSource implements IAsyncDataSource<ITreeItem, ITreeItem> {11871188constructor(1189private treeView: ITreeView,1190private withProgress: <T>(task: Promise<T>) => Promise<T>1191) {1192}11931194hasChildren(element: ITreeItem): boolean {1195return !!this.treeView.dataProvider && (element.collapsibleState !== TreeItemCollapsibleState.None);1196}11971198private batch: ITreeItem[] | undefined;1199private batchPromise: Promise<ITreeItem[][] | undefined> | undefined;1200async getChildren(element: ITreeItem): Promise<ITreeItem[]> {1201const dataProvider = this.treeView.dataProvider;1202if (!dataProvider) {1203return [];1204}1205if (this.batch === undefined) {1206this.batch = [element];1207this.batchPromise = undefined;1208} else {1209this.batch.push(element);1210}1211const indexInBatch = this.batch.length - 1;1212return new Promise<ITreeItem[]>((resolve, reject) => {1213setTimeout(async () => {1214const batch = this.batch;1215this.batch = undefined;1216if (!this.batchPromise) {1217this.batchPromise = this.withProgress(doGetChildrenOrBatch(dataProvider, batch));1218}1219try {1220const result = await this.batchPromise;1221resolve((result && (indexInBatch < result.length)) ? result[indexInBatch] : []);1222} catch (e) {1223if (!(<string>e.message).startsWith('Bad progress location:')) {1224reject(e);1225}1226}1227}, 0);1228});1229}1230}12311232interface ITreeExplorerTemplateData {1233readonly container: HTMLElement;1234readonly resourceLabel: IResourceLabel;1235readonly icon: HTMLElement;1236readonly checkboxContainer: HTMLElement;1237checkbox?: TreeItemCheckbox;1238readonly actionBar: ActionBar;1239}12401241class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyScore, ITreeExplorerTemplateData> {1242static readonly ITEM_HEIGHT = 22;1243static readonly TREE_TEMPLATE_ID = 'treeExplorer';12441245private readonly _onDidChangeCheckboxState: Emitter<readonly ITreeItem[]> = this._register(new Emitter<readonly ITreeItem[]>());1246readonly onDidChangeCheckboxState: Event<readonly ITreeItem[]> = this._onDidChangeCheckboxState.event;12471248private _onDidChangeMenuContext: Emitter<readonly ITreeItem[]> = this._register(new Emitter<readonly ITreeItem[]>());1249readonly onDidChangeMenuContext: Event<readonly ITreeItem[]> = this._onDidChangeMenuContext.event;12501251private _actionRunner: MultipleSelectionActionRunner | undefined;1252private _hoverDelegate: IHoverDelegate;1253private _hasCheckbox: boolean = false;1254private _renderedElements = new Map<string, { original: ITreeNode<ITreeItem, FuzzyScore>; rendered: ITreeExplorerTemplateData }[]>(); // tree item handle to template data12551256constructor(1257private treeViewId: string,1258private menus: TreeMenus,1259private labels: ResourceLabels,1260private actionViewItemProvider: IActionViewItemProvider,1261private aligner: Aligner,1262private checkboxStateHandler: CheckboxStateHandler,1263private readonly manuallyManageCheckboxes: () => boolean,1264@IThemeService private readonly themeService: IThemeService,1265@IConfigurationService private readonly configurationService: IConfigurationService,1266@ILabelService private readonly labelService: ILabelService,1267@IContextKeyService private readonly contextKeyService: IContextKeyService,1268@IHoverService private readonly hoverService: IHoverService,1269@IInstantiationService instantiationService: IInstantiationService,1270) {1271super();1272this._hoverDelegate = this._register(instantiationService.createInstance(WorkbenchHoverDelegate, 'mouse', undefined, {}));1273this._register(this.themeService.onDidFileIconThemeChange(() => this.rerender()));1274this._register(this.themeService.onDidColorThemeChange(() => this.rerender()));1275this._register(checkboxStateHandler.onDidChangeCheckboxState(items => {1276this.updateCheckboxes(items);1277}));1278this._register(this.contextKeyService.onDidChangeContext(e => this.onDidChangeContext(e)));1279}12801281get templateId(): string {1282return TreeRenderer.TREE_TEMPLATE_ID;1283}12841285set actionRunner(actionRunner: MultipleSelectionActionRunner) {1286this._actionRunner = actionRunner;1287}12881289renderTemplate(container: HTMLElement): ITreeExplorerTemplateData {1290container.classList.add('custom-view-tree-node-item');12911292const checkboxContainer = DOM.append(container, DOM.$(''));1293const resourceLabel = this.labels.create(container, { supportHighlights: true, hoverDelegate: this._hoverDelegate });1294const icon = DOM.prepend(resourceLabel.element, DOM.$('.custom-view-tree-node-item-icon'));1295const actionsContainer = DOM.append(resourceLabel.element, DOM.$('.actions'));1296const actionBar = new ActionBar(actionsContainer, {1297actionViewItemProvider: this.actionViewItemProvider1298});12991300return { resourceLabel, icon, checkboxContainer, actionBar, container };1301}13021303private getHover(label: string | IMarkdownString | undefined, resource: URI | null, node: ITreeItem): string | IManagedHoverTooltipMarkdownString | undefined {1304if (!(node instanceof ResolvableTreeItem) || !node.hasResolve) {1305if (resource && !node.tooltip) {1306return undefined;1307} else if (node.tooltip === undefined) {1308if (isMarkdownString(label)) {1309return { markdown: label, markdownNotSupportedFallback: label.value };1310} else {1311return label;1312}1313} else if (!isString(node.tooltip)) {1314return { markdown: node.tooltip, markdownNotSupportedFallback: resource ? undefined : renderAsPlaintext(node.tooltip) }; // Passing undefined as the fallback for a resource falls back to the old native hover1315} else if (node.tooltip !== '') {1316return node.tooltip;1317} else {1318return undefined;1319}1320}13211322return {1323markdown: typeof node.tooltip === 'string' ? node.tooltip :1324(token: CancellationToken): Promise<IMarkdownString | string | undefined> => {1325return new Promise<IMarkdownString | string | undefined>((resolve) => {1326node.resolve(token).then(() => resolve(node.tooltip));1327});1328},1329markdownNotSupportedFallback: resource ? undefined : (label ? (isMarkdownString(label) ? label.value : label) : '') // Passing undefined as the fallback for a resource falls back to the old native hover1330};1331}13321333private processLabel(label: string | IMarkdownString | undefined, matches: { start: number; end: number }[] | undefined): { label: string | undefined; bold?: boolean; italic?: boolean; strikethrough?: boolean; supportIcons?: boolean } {1334if (!isMarkdownString(label)) {1335return { label };1336}13371338let text = label.value.trim();1339let bold = false;1340let italic = false;1341let strikethrough = false;13421343function moveMatches(offset: number) {1344if (matches) {1345for (const match of matches) {1346match.start -= offset;1347match.end -= offset;1348}1349}1350}13511352const syntaxes = [1353{ open: '~~', close: '~~', mark: () => { strikethrough = true; } },1354{ open: '**', close: '**', mark: () => { bold = true; } },1355{ open: '*', close: '*', mark: () => { italic = true; } },1356{ open: '_', close: '_', mark: () => { italic = true; } }1357];13581359function checkSyntaxes(): boolean {1360let didChange = false;1361for (const syntax of syntaxes) {1362if (text.startsWith(syntax.open) && text.endsWith(syntax.close)) {1363// If there is a match within the markers, stop processing1364if (matches?.some(match => match.start < syntax.open.length || match.end > text.length - syntax.close.length)) {1365return false;1366}13671368syntax.mark();1369text = text.substring(syntax.open.length, text.length - syntax.close.length);1370moveMatches(syntax.open.length);1371didChange = true;1372}1373}1374return didChange;1375}13761377// Arbitrary max # of iterations1378for (let i = 0; i < 10; i++) {1379if (!checkSyntaxes()) {1380break;1381}1382}13831384return {1385label: text,1386bold,1387italic,1388strikethrough,1389supportIcons: label.supportThemeIcons1390};1391}13921393renderElement(element: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {1394const node = element.element;1395const resource = node.resourceUri ? URI.revive(node.resourceUri) : null;1396const treeItemLabel: ITreeItemLabel | undefined = node.label ? node.label : (resource ? { label: basename(resource) } : undefined);1397const description = isString(node.description) ? node.description : resource && node.description === true ? this.labelService.getUriLabel(dirname(resource), { relative: true }) : undefined;1398const labelStr = treeItemLabel ? isMarkdownString(treeItemLabel.label) ? treeItemLabel.label.value : treeItemLabel.label : undefined;1399const matches = (treeItemLabel?.highlights && labelStr) ? treeItemLabel.highlights.map(([start, end]) => {1400if (start < 0) {1401start = labelStr.length + start;1402}1403if (end < 0) {1404end = labelStr.length + end;1405}1406if ((start >= labelStr.length) || (end > labelStr.length)) {1407return ({ start: 0, end: 0 });1408}1409if (start > end) {1410const swap = start;1411start = end;1412end = swap;1413}1414return ({ start, end });1415}) : undefined;1416const { label, bold, italic, strikethrough, supportIcons } = this.processLabel(treeItemLabel?.label, matches);1417const icon = !isDark(this.themeService.getColorTheme().type) ? node.icon : node.iconDark;1418const iconUrl = icon ? URI.revive(icon) : undefined;1419const title = this.getHover(treeItemLabel?.label, resource, node);14201421// reset1422templateData.actionBar.clear();1423templateData.icon.style.color = '';14241425let commandEnabled = true;1426if (node.command) {1427commandEnabled = isTreeCommandEnabled(node.command, this.contextKeyService);1428}14291430this.renderCheckbox(node, templateData);14311432if (resource) {1433const fileDecorations = this.configurationService.getValue<{ colors: boolean; badges: boolean }>('explorer.decorations');1434const labelResource = resource ? resource : URI.parse('missing:_icon_resource');1435templateData.resourceLabel.setResource({ name: label, description, resource: labelResource }, {1436fileKind: this.getFileKind(node),1437title,1438hideIcon: this.shouldHideResourceLabelIcon(iconUrl, node.themeIcon),1439fileDecorations,1440extraClasses: ['custom-view-tree-node-item-resourceLabel'],1441matches: matches ? matches : createMatches(element.filterData),1442bold,1443italic,1444strikethrough,1445disabledCommand: !commandEnabled,1446labelEscapeNewLines: true,1447forceLabel: !!node.label,1448supportIcons1449});1450} else {1451templateData.resourceLabel.setResource({ name: label, description }, {1452title,1453hideIcon: true,1454extraClasses: ['custom-view-tree-node-item-resourceLabel'],1455matches: matches ? matches : createMatches(element.filterData),1456bold,1457italic,1458strikethrough,1459disabledCommand: !commandEnabled,1460labelEscapeNewLines: true,1461supportIcons1462});1463}14641465if (iconUrl) {1466templateData.icon.className = 'custom-view-tree-node-item-icon';1467templateData.icon.style.backgroundImage = cssJs.asCSSUrl(iconUrl);1468} else {1469let iconClass: string | undefined;1470if (this.shouldShowThemeIcon(!!resource, node.themeIcon)) {1471iconClass = ThemeIcon.asClassName(node.themeIcon);1472if (node.themeIcon.color) {1473templateData.icon.style.color = this.themeService.getColorTheme().getColor(node.themeIcon.color.id)?.toString() ?? '';1474}1475}1476templateData.icon.className = iconClass ? `custom-view-tree-node-item-icon ${iconClass}` : '';1477templateData.icon.style.backgroundImage = '';1478}14791480if (!commandEnabled) {1481templateData.icon.className = templateData.icon.className + ' disabled';1482if (templateData.container.parentElement) {1483templateData.container.parentElement.className = templateData.container.parentElement.className + ' disabled';1484}1485}14861487templateData.actionBar.context = { $treeViewId: this.treeViewId, $treeItemHandle: node.handle } satisfies TreeViewItemHandleArg;14881489const menuActions = this.menus.getResourceActions([node]);1490templateData.actionBar.push(menuActions, { icon: true, label: false });14911492if (this._actionRunner) {1493templateData.actionBar.actionRunner = this._actionRunner;1494}1495this.setAlignment(templateData.container, node);14961497// remember rendered element, an element can be rendered multiple times1498const renderedItems = this._renderedElements.get(element.element.handle) ?? [];1499this._renderedElements.set(element.element.handle, [...renderedItems, { original: element, rendered: templateData }]);1500}15011502private rerender() {1503// As we add items to the map during this call we can't directly use the map in the for loop1504// but have to create a copy of the keys first1505const keys = new Set(this._renderedElements.keys());1506for (const key of keys) {1507const values = this._renderedElements.get(key) ?? [];1508for (const value of values) {1509this.disposeElement(value.original, 0, value.rendered);1510this.renderElement(value.original, 0, value.rendered);1511}1512}1513}15141515private renderCheckbox(node: ITreeItem, templateData: ITreeExplorerTemplateData) {1516if (node.checkbox) {1517// The first time we find a checkbox we want to rerender the visible tree to adapt the alignment1518if (!this._hasCheckbox) {1519this._hasCheckbox = true;1520this.rerender();1521}1522if (!templateData.checkbox) {1523const checkbox = new TreeItemCheckbox(templateData.checkboxContainer, this.checkboxStateHandler, this._hoverDelegate, this.hoverService);1524templateData.checkbox = checkbox;1525}1526templateData.checkbox.render(node);1527} else if (templateData.checkbox) {1528templateData.checkbox.dispose();1529templateData.checkbox = undefined;1530}1531}15321533private setAlignment(container: HTMLElement, treeItem: ITreeItem) {1534container.parentElement!.classList.toggle('align-icon-with-twisty', this.aligner.alignIconWithTwisty(treeItem));1535}15361537private shouldHideResourceLabelIcon(iconUrl: URI | undefined, icon: ThemeIcon | undefined): boolean {1538// We always hide the resource label in favor of the iconUrl when it's provided.1539// When `ThemeIcon` is provided, we hide the resource label icon in favor of it only if it's a not a file icon.1540return (!!iconUrl || (!!icon && !this.isFileKindThemeIcon(icon)));1541}15421543private shouldShowThemeIcon(hasResource: boolean, icon: ThemeIcon | undefined): icon is ThemeIcon {1544if (!icon) {1545return false;1546}15471548// If there's a resource and the icon is a file icon, then the icon (or lack thereof) will already be coming from the1549// icon theme and should use whatever the icon theme has provided.1550return !(hasResource && this.isFileKindThemeIcon(icon));1551}15521553private isFileKindThemeIcon(icon: ThemeIcon | undefined): boolean {1554return ThemeIcon.isFile(icon) || ThemeIcon.isFolder(icon);1555}15561557private getFileKind(node: ITreeItem): FileKind {1558if (node.themeIcon) {1559switch (node.themeIcon.id) {1560case FileThemeIcon.id:1561return FileKind.FILE;1562case FolderThemeIcon.id:1563return FileKind.FOLDER;1564}1565}1566return node.collapsibleState === TreeItemCollapsibleState.Collapsed || node.collapsibleState === TreeItemCollapsibleState.Expanded ? FileKind.FOLDER : FileKind.FILE;1567}15681569private onDidChangeContext(e: IContextKeyChangeEvent) {1570const affectsEntireMenuContexts = e.affectsSome(this.menus.getEntireMenuContexts());15711572const items: ITreeItem[] = [];1573for (const [_, elements] of this._renderedElements) {1574for (const element of elements) {1575if (affectsEntireMenuContexts || e.affectsSome(this.menus.getElementOverlayContexts(element.original.element))) {1576items.push(element.original.element);1577}1578}1579}1580if (items.length) {1581this._onDidChangeMenuContext.fire(items);1582}1583}15841585private updateCheckboxes(items: ITreeItem[]) {1586let allItems: ITreeItem[] = [];15871588if (!this.manuallyManageCheckboxes()) {1589allItems = setCascadingCheckboxUpdates(items);1590} else {1591allItems = items;1592}15931594allItems.forEach(item => {1595const renderedItems = this._renderedElements.get(item.handle);1596if (renderedItems) {1597renderedItems.forEach(renderedItems => renderedItems.rendered.checkbox?.render(item));1598}1599});1600this._onDidChangeCheckboxState.fire(allItems);1601}16021603disposeElement(resource: ITreeNode<ITreeItem, FuzzyScore>, index: number, templateData: ITreeExplorerTemplateData): void {1604const itemRenders = this._renderedElements.get(resource.element.handle) ?? [];1605const renderedIndex = itemRenders.findIndex(renderedItem => templateData === renderedItem.rendered);16061607if (itemRenders.length === 1) {1608this._renderedElements.delete(resource.element.handle);1609} else if (itemRenders.length > 0) {1610itemRenders.splice(renderedIndex, 1);1611}16121613templateData.checkbox?.dispose();1614templateData.checkbox = undefined;1615}16161617disposeTemplate(templateData: ITreeExplorerTemplateData): void {1618templateData.resourceLabel.dispose();1619templateData.actionBar.dispose();1620}1621}16221623class Aligner extends Disposable {1624private _tree: WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> | undefined;16251626constructor(private themeService: IThemeService, private logService: ILogService) {1627super();1628}16291630set tree(tree: WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore>) {1631this._tree = tree;1632}16331634public alignIconWithTwisty(treeItem: ITreeItem): boolean {1635if (treeItem.collapsibleState !== TreeItemCollapsibleState.None) {1636return false;1637}1638if (!this.hasIconOrCheckbox(treeItem)) {1639return false;1640}16411642if (this._tree) {1643const root = this._tree.getInput();1644let parent: ITreeItem;1645try {1646parent = this._tree.getParentElement(treeItem) || root;1647} catch (error) {1648this.logService.error(`[TreeView] Failed to resolve parent for ${treeItem.handle}`, error);1649return false;1650}1651if (this.hasIconOrCheckbox(parent)) {1652return !!parent.children && parent.children.some(c => c.collapsibleState !== TreeItemCollapsibleState.None && !this.hasIconOrCheckbox(c));1653}1654return !!parent.children && parent.children.every(c => c.collapsibleState === TreeItemCollapsibleState.None || !this.hasIconOrCheckbox(c));1655} else {1656return false;1657}1658}16591660private hasIconOrCheckbox(node: ITreeItem): boolean {1661return this.hasIcon(node) || !!node.checkbox;1662}16631664private hasIcon(node: ITreeItem): boolean {1665const icon = !isDark(this.themeService.getColorTheme().type) ? node.icon : node.iconDark;1666if (icon) {1667return true;1668}1669if (node.resourceUri || node.themeIcon) {1670const fileIconTheme = this.themeService.getFileIconTheme();1671const isFolder = node.themeIcon ? node.themeIcon.id === FolderThemeIcon.id : node.collapsibleState !== TreeItemCollapsibleState.None;1672if (isFolder) {1673return fileIconTheme.hasFileIcons && fileIconTheme.hasFolderIcons;1674}1675return fileIconTheme.hasFileIcons;1676}1677return false;1678}1679}16801681class MultipleSelectionActionRunner extends ActionRunner {16821683constructor(notificationService: INotificationService, private getSelectedResources: (() => ITreeItem[])) {1684super();1685this._register(this.onDidRun(e => {1686if (e.error && !isCancellationError(e.error)) {1687notificationService.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));1688}1689}));1690}16911692protected override async runAction(action: IAction, context: TreeViewItemHandleArg | TreeViewPaneHandleArg): Promise<void> {1693const selection = this.getSelectedResources();1694let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined;1695let actionInSelected: boolean = false;1696if (selection.length > 1) {1697selectionHandleArgs = selection.map(selected => {1698if ((selected.handle === (context as TreeViewItemHandleArg).$treeItemHandle) || (context as TreeViewPaneHandleArg).$selectedTreeItems) {1699actionInSelected = true;1700}1701return { $treeViewId: context.$treeViewId, $treeItemHandle: selected.handle };1702});1703}17041705if (!actionInSelected && selectionHandleArgs) {1706selectionHandleArgs = undefined;1707}17081709await action.run(context, selectionHandleArgs);1710}1711}17121713class TreeMenus implements IDisposable {1714private contextKeyService: IContextKeyService | undefined;1715private _onDidChange = new Emitter<ITreeItem>();1716public readonly onDidChange = this._onDidChange.event;17171718constructor(1719private id: string,1720@IMenuService private readonly menuService: IMenuService1721) { }17221723/**1724* Gets only the actions that apply to all of the given elements.1725*/1726getResourceActions(elements: ITreeItem[]): IAction[] {1727const actions = this.getActions(this.getMenuId(), elements);1728return actions.primary;1729}17301731/**1732* Gets only the actions that apply to all of the given elements.1733*/1734getResourceContextActions(elements: ITreeItem[]): IAction[] {1735return this.getActions(this.getMenuId(), elements).secondary;1736}17371738public setContextKeyService(service: IContextKeyService) {1739this.contextKeyService = service;1740}17411742private filterNonUniversalActions(groups: Map<string, IAction>[], newActions: IAction[]) {1743const newActionsSet: Set<string> = new Set(newActions.map(a => a.id));1744for (const group of groups) {1745const actions = group.keys();1746for (const action of actions) {1747if (!newActionsSet.has(action)) {1748group.delete(action);1749}1750}1751}1752}17531754private buildMenu(groups: Map<string, IAction>[]): IAction[] {1755const result: IAction[] = [];1756for (const group of groups) {1757if (group.size > 0) {1758if (result.length) {1759result.push(new Separator());1760}1761result.push(...group.values());1762}1763}1764return result;1765}17661767private createGroups(actions: IAction[]): Map<string, IAction>[] {1768const groups: Map<string, IAction>[] = [];1769let group: Map<string, IAction> = new Map();1770for (const action of actions) {1771if (action instanceof Separator) {1772groups.push(group);1773group = new Map();1774} else {1775group.set(action.id, action);1776}1777}1778groups.push(group);1779return groups;1780}17811782public getElementOverlayContexts(element: ITreeItem): Map<string, unknown> {1783return new Map([1784['view', this.id],1785['viewItem', element.contextValue]1786]);1787}17881789public getEntireMenuContexts(): ReadonlySet<string> {1790return this.menuService.getMenuContexts(this.getMenuId());1791}17921793public getMenuId(): MenuId {1794return MenuId.ViewItemContext;1795}17961797private getActions(menuId: MenuId, elements: ITreeItem[]): { primary: IAction[]; secondary: IAction[] } {1798if (!this.contextKeyService) {1799return { primary: [], secondary: [] };1800}18011802let primaryGroups: Map<string, IAction>[] = [];1803let secondaryGroups: Map<string, IAction>[] = [];1804for (let i = 0; i < elements.length; i++) {1805const element = elements[i];1806const contextKeyService = this.contextKeyService.createOverlay(this.getElementOverlayContexts(element));18071808const menuData = this.menuService.getMenuActions(menuId, contextKeyService, { shouldForwardArgs: true });18091810const result = getContextMenuActions(menuData, 'inline');1811if (i === 0) {1812primaryGroups = this.createGroups(result.primary);1813secondaryGroups = this.createGroups(result.secondary);1814} else {1815this.filterNonUniversalActions(primaryGroups, result.primary);1816this.filterNonUniversalActions(secondaryGroups, result.secondary);1817}1818}18191820return { primary: this.buildMenu(primaryGroups), secondary: this.buildMenu(secondaryGroups) };1821}18221823dispose() {1824this.contextKeyService = undefined;1825this._onDidChange.dispose();1826}1827}18281829export class CustomTreeView extends AbstractTreeView {18301831constructor(1832id: string,1833title: string,1834private readonly extensionId: string,1835@IThemeService themeService: IThemeService,1836@IInstantiationService instantiationService: IInstantiationService,1837@ICommandService commandService: ICommandService,1838@IConfigurationService configurationService: IConfigurationService,1839@IProgressService progressService: IProgressService,1840@IContextMenuService contextMenuService: IContextMenuService,1841@IKeybindingService keybindingService: IKeybindingService,1842@INotificationService notificationService: INotificationService,1843@IViewDescriptorService viewDescriptorService: IViewDescriptorService,1844@IContextKeyService contextKeyService: IContextKeyService,1845@IHoverService hoverService: IHoverService,1846@IExtensionService private readonly extensionService: IExtensionService,1847@IActivityService activityService: IActivityService,1848@ITelemetryService private readonly telemetryService: ITelemetryService,1849@ILogService logService: ILogService,1850@IOpenerService openerService: IOpenerService,1851@IMarkdownRendererService markdownRendererService: IMarkdownRendererService,1852) {1853super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, hoverService, contextKeyService, activityService, logService, openerService, markdownRendererService);1854}18551856protected activate() {1857if (!this.activated) {1858type ExtensionViewTelemetry = {1859extensionId: TelemetryTrustedValue<string>;1860id: string;1861};1862type ExtensionViewTelemetryMeta = {1863extensionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the extension' };1864id: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Id of the view' };1865owner: 'digitarald';1866comment: 'Helps to gain insights on what extension contributed views are most popular';1867};1868this.telemetryService.publicLog2<ExtensionViewTelemetry, ExtensionViewTelemetryMeta>('Extension:ViewActivate', {1869extensionId: new TelemetryTrustedValue(this.extensionId),1870id: this.id,1871});1872this.createTree();1873this.progressService.withProgress({ location: this.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`))1874.then(() => timeout(2000))1875.then(() => {1876this.updateMessage();1877});1878this.activated = true;1879}1880}1881}18821883export class TreeView extends AbstractTreeView {18841885protected activate() {1886if (!this.activated) {1887this.createTree();1888this.activated = true;1889}1890}1891}18921893interface TreeDragSourceInfo {1894id: string;1895itemHandles: string[];1896}18971898export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop<ITreeItem> {1899private readonly treeMimeType: string;1900private readonly treeItemsTransfer = LocalSelectionTransfer.getInstance<DraggedTreeItemsIdentifier>();1901private dragCancellationToken: CancellationTokenSource | undefined;19021903constructor(1904private readonly treeId: string,1905@ILabelService private readonly labelService: ILabelService,1906@IInstantiationService private readonly instantiationService: IInstantiationService,1907@ITreeViewsDnDService private readonly treeViewsDragAndDropService: ITreeViewsDnDService,1908@ILogService private readonly logService: ILogService) {1909this.treeMimeType = `application/vnd.code.tree.${treeId.toLowerCase()}`;1910}19111912private dndController: ITreeViewDragAndDropController | undefined;1913set controller(controller: ITreeViewDragAndDropController | undefined) {1914this.dndController = controller;1915}19161917private handleDragAndLog(dndController: ITreeViewDragAndDropController, itemHandles: string[], uuid: string, dragCancellationToken: CancellationToken): Promise<VSDataTransfer | undefined> {1918return dndController.handleDrag(itemHandles, uuid, dragCancellationToken).then(additionalDataTransfer => {1919if (additionalDataTransfer) {1920const unlistedTypes: string[] = [];1921for (const item of additionalDataTransfer) {1922if ((item[0] !== this.treeMimeType) && (dndController.dragMimeTypes.findIndex(value => value === item[0]) < 0)) {1923unlistedTypes.push(item[0]);1924}1925}1926if (unlistedTypes.length) {1927this.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(', ')}`);1928}1929}1930return additionalDataTransfer;1931});1932}19331934private addExtensionProvidedTransferTypes(originalEvent: DragEvent, itemHandles: string[]) {1935if (!originalEvent.dataTransfer || !this.dndController) {1936return;1937}1938const uuid = generateUuid();19391940this.dragCancellationToken = new CancellationTokenSource();1941this.treeViewsDragAndDropService.addDragOperationTransfer(uuid, this.handleDragAndLog(this.dndController, itemHandles, uuid, this.dragCancellationToken.token));1942this.treeItemsTransfer.setData([new DraggedTreeItemsIdentifier(uuid)], DraggedTreeItemsIdentifier.prototype);1943originalEvent.dataTransfer.clearData(Mimes.text);1944if (this.dndController.dragMimeTypes.find((element) => element === Mimes.uriList)) {1945// Add the type that the editor knows1946originalEvent.dataTransfer?.setData(DataTransfers.RESOURCES, '');1947}1948this.dndController.dragMimeTypes.forEach(supportedType => {1949originalEvent.dataTransfer?.setData(supportedType, '');1950});1951}19521953private addResourceInfoToTransfer(originalEvent: DragEvent, resources: URI[]) {1954if (resources.length && originalEvent.dataTransfer) {1955// Apply some datatransfer types to allow for dragging the element outside of the application1956this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, resources, originalEvent));19571958// The only custom data transfer we set from the explorer is a file transfer1959// to be able to DND between multiple code file explorers across windows1960const fileResources = resources.filter(s => s.scheme === Schemas.file).map(r => r.fsPath);1961if (fileResources.length) {1962originalEvent.dataTransfer.setData(CodeDataTransfers.FILES, JSON.stringify(fileResources));1963}1964}1965}19661967onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {1968if (originalEvent.dataTransfer) {1969const treeItemsData = (data as ElementsDragAndDropData<ITreeItem, ITreeItem[]>).getData();1970const resources: URI[] = [];1971const sourceInfo: TreeDragSourceInfo = {1972id: this.treeId,1973itemHandles: []1974};1975treeItemsData.forEach(item => {1976sourceInfo.itemHandles.push(item.handle);1977if (item.resourceUri) {1978resources.push(URI.revive(item.resourceUri));1979}1980});1981this.addResourceInfoToTransfer(originalEvent, resources);1982this.addExtensionProvidedTransferTypes(originalEvent, sourceInfo.itemHandles);1983originalEvent.dataTransfer.setData(this.treeMimeType,1984JSON.stringify(sourceInfo));1985}1986}19871988private debugLog(types: Set<string>) {1989if (types.size) {1990this.logService.debug(`TreeView dragged mime types: ${Array.from(types).join(', ')}`);1991} else {1992this.logService.debug(`TreeView dragged with no supported mime types.`);1993}1994}19951996onDragOver(data: IDragAndDropData, targetElement: ITreeItem, targetIndex: number, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {1997const dataTransfer = toExternalVSDataTransfer(originalEvent.dataTransfer!);19981999const types = new Set<string>(Array.from(dataTransfer, x => x[0]));20002001if (originalEvent.dataTransfer) {2002// Also add uri-list if we have any files. At this stage we can't actually access the file itself though.2003for (const item of originalEvent.dataTransfer.items) {2004if (item.kind === 'file' || item.type === DataTransfers.RESOURCES.toLowerCase()) {2005types.add(Mimes.uriList);2006break;2007}2008}2009}20102011this.debugLog(types);20122013const dndController = this.dndController;2014if (!dndController || !originalEvent.dataTransfer || (dndController.dropMimeTypes.length === 0)) {2015return false;2016}2017const dragContainersSupportedType = Array.from(types).some((value, index) => {2018if (value === this.treeMimeType) {2019return true;2020} else {2021return dndController.dropMimeTypes.indexOf(value) >= 0;2022}2023});2024if (dragContainersSupportedType) {2025return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: true };2026}2027return false;2028}20292030getDragURI(element: ITreeItem): string | null {2031if (!this.dndController) {2032return null;2033}2034return element.resourceUri ? URI.revive(element.resourceUri).toString() : element.handle;2035}20362037getDragLabel?(elements: ITreeItem[]): string | undefined {2038if (!this.dndController) {2039return undefined;2040}2041if (elements.length > 1) {2042return String(elements.length);2043}2044const element = elements[0];2045if (element.label) {2046return isMarkdownString(element.label.label) ? element.label.label.value : element.label.label;2047}2048return element.resourceUri ? this.labelService.getUriLabel(URI.revive(element.resourceUri)) : undefined;2049}20502051async drop(data: IDragAndDropData, targetNode: ITreeItem | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): Promise<void> {2052const dndController = this.dndController;2053if (!originalEvent.dataTransfer || !dndController) {2054return;2055}20562057let treeSourceInfo: TreeDragSourceInfo | undefined;2058let willDropUuid: string | undefined;2059if (this.treeItemsTransfer.hasData(DraggedTreeItemsIdentifier.prototype)) {2060willDropUuid = this.treeItemsTransfer.getData(DraggedTreeItemsIdentifier.prototype)![0].identifier;2061}20622063const originalDataTransfer = toExternalVSDataTransfer(originalEvent.dataTransfer, true);20642065const outDataTransfer = new VSDataTransfer();2066for (const [type, item] of originalDataTransfer) {2067if (type === this.treeMimeType || dndController.dropMimeTypes.includes(type) || (item.asFile() && dndController.dropMimeTypes.includes(DataTransfers.FILES.toLowerCase()))) {2068outDataTransfer.append(type, item);2069if (type === this.treeMimeType) {2070try {2071treeSourceInfo = JSON.parse(await item.asString());2072} catch {2073// noop2074}2075}2076}2077}20782079const additionalDataTransfer = await this.treeViewsDragAndDropService.removeDragOperationTransfer(willDropUuid);2080if (additionalDataTransfer) {2081for (const [type, item] of additionalDataTransfer) {2082outDataTransfer.append(type, item);2083}2084}2085return dndController.handleDrop(outDataTransfer, targetNode, CancellationToken.None, willDropUuid, treeSourceInfo?.id, treeSourceInfo?.itemHandles);2086}20872088onDragEnd(originalEvent: DragEvent): void {2089// Check if the drag was cancelled.2090if (originalEvent.dataTransfer?.dropEffect === 'none') {2091this.dragCancellationToken?.cancel();2092}2093}20942095dispose(): void { }2096}20972098function setCascadingCheckboxUpdates(items: readonly ITreeItem[]) {2099const additionalItems: ITreeItem[] = [];21002101for (const item of items) {2102if (item.checkbox !== undefined) {21032104const checkChildren = (currentItem: ITreeItem) => {2105for (const child of (currentItem.children ?? [])) {2106if ((child.checkbox !== undefined) && (currentItem.checkbox !== undefined) && (child.checkbox.isChecked !== currentItem.checkbox.isChecked)) {2107child.checkbox.isChecked = currentItem.checkbox.isChecked;2108additionalItems.push(child);2109checkChildren(child);2110}2111}2112};2113checkChildren(item);21142115const visitedParents: Set<ITreeItem> = new Set();2116const checkParents = (currentItem: ITreeItem) => {2117if (currentItem.parent?.checkbox !== undefined && currentItem.parent.children) {2118if (visitedParents.has(currentItem.parent)) {2119return;2120} else {2121visitedParents.add(currentItem.parent);2122}21232124let someUnchecked = false;2125let someChecked = false;2126for (const child of currentItem.parent.children) {2127if (someUnchecked && someChecked) {2128break;2129}2130if (child.checkbox !== undefined) {2131if (child.checkbox.isChecked) {2132someChecked = true;2133} else {2134someUnchecked = true;2135}2136}2137}2138if (someChecked && !someUnchecked && (currentItem.parent.checkbox.isChecked !== true)) {2139currentItem.parent.checkbox.isChecked = true;2140additionalItems.push(currentItem.parent);2141checkParents(currentItem.parent);2142} else if (someUnchecked && (currentItem.parent.checkbox.isChecked !== false)) {2143currentItem.parent.checkbox.isChecked = false;2144additionalItems.push(currentItem.parent);2145checkParents(currentItem.parent);2146}2147}2148};2149checkParents(item);2150}2151}21522153return items.concat(additionalItems);2154}215521562157