diff --git a/extensions/notebook/src/test/common/stubs.ts b/extensions/notebook/src/test/common/stubs.ts index 3ca2709278..e8d0580121 100644 --- a/extensions/notebook/src/test/common/stubs.ts +++ b/extensions/notebook/src/test/common/stubs.ts @@ -10,6 +10,7 @@ export interface ExtensionGlobalMemento extends vscode.Memento { } export class MockExtensionContext implements vscode.ExtensionContext { + extensionRuntime = 1; logger: undefined; logPath: './'; subscriptions: { dispose(): any; }[]; diff --git a/extensions/notebook/src/typings/refs.d.ts b/extensions/notebook/src/typings/refs.d.ts index b128c06b40..8b612c7670 100644 --- a/extensions/notebook/src/typings/refs.d.ts +++ b/extensions/notebook/src/typings/refs.d.ts @@ -6,5 +6,6 @@ /// /// /// +/// /// /// diff --git a/src/sql/workbench/api/common/extHostModelViewTree.ts b/src/sql/workbench/api/common/extHostModelViewTree.ts index 0b6134fc7a..9193284352 100644 --- a/src/sql/workbench/api/common/extHostModelViewTree.ts +++ b/src/sql/workbench/api/common/extHostModelViewTree.ts @@ -16,6 +16,7 @@ import { Emitter } from 'vs/base/common/event'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { assign } from 'vs/base/common/objects'; import { ILogService } from 'vs/platform/log/common/log'; +import { TreeDataTransferDTO } from 'vs/workbench/api/common/shared/treeDataTransfer'; export class ExtHostModelViewTreeViews implements ExtHostModelViewTreeViewsShape { private _proxy: MainThreadModelViewShape; @@ -85,6 +86,10 @@ export class ExtHostModelViewTreeViews implements ExtHostModelViewTreeViewsShape return Promise.resolve(undefined); } + $onDrop(treeViewId: string, treeDataTransferDTO: TreeDataTransferDTO, newParentItemHandle: string): Promise { + return Promise.resolve(undefined); + } + private createExtHostTreeViewer(handle: number, id: string, dataProvider: azdata.TreeComponentDataProvider, extension: IExtensionDescription, logService: ILogService): ExtHostTreeView { const treeView = new ExtHostTreeView(handle, id, dataProvider, this._proxy, undefined, extension, logService); this.treeViews.set(`${handle}-${id}`, treeView); diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index beed0eec42..f00376a43c 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -26,6 +26,7 @@ import { IUndoStopOptions } from 'vs/workbench/api/common/extHost.protocol'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IQueryEvent } from 'sql/workbench/services/query/common/queryModel'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; +import { TreeDataTransferDTO } from 'vs/workbench/api/common/shared/treeDataTransfer'; export abstract class ExtHostAccountManagementShape { $autoOAuthCancelled(handle: number): Thenable { throw ni(); } @@ -740,6 +741,7 @@ export interface ExtHostModelViewShape { export interface ExtHostModelViewTreeViewsShape { $getChildren(treeViewId: string, treeItemHandle?: string): Promise; + $onDrop(treeViewId: string, treeDataTransfer: TreeDataTransferDTO, newParentTreeItemHandle: string): Promise; $createTreeView(handle: number, componentId: string, options: { treeDataProvider: vscode.TreeDataProvider }, extension: IExtensionDescription): azdata.TreeComponentView; $onNodeCheckedChanged(treeViewId: string, treeItemHandle?: string, checked?: boolean): void; $onNodeSelected(treeViewId: string, nodes: string[]): void; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 19ea6bdef9..52a4e17b77 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -881,6 +881,40 @@ declare module 'vscode' { } //#endregion + //#region Custom Tree View Drag and Drop https://github.com/microsoft/vscode/issues/32592 + export interface TreeViewOptions { + dragAndDropController?: DragAndDropController; + } + + export interface TreeDataTransferItem { + asString(): Thenable; + } + + export interface TreeDataTransfer { + /** + * A map containing a mapping of the mime type of the corresponding data. + * The type for tree elements is text/treeitem. + * For example, you can reconstruct the your tree elements: + * ```ts + * JSON.parse(await (items.get('text/treeitems')!.asString())) + * ``` + */ + items: Map; + } + + export interface DragAndDropController extends Disposable { + readonly supportedTypes: string[]; + + /** + * Extensions should fire `TreeDataProvider.onDidChangeTreeData` for any elements that need to be refreshed. + * + * @param source + * @param target + */ + onDrop(source: TreeDataTransfer, target: T): Thenable; + } + //#endregion + //#region Task presentation group: https://github.com/microsoft/vscode/issues/47265 export interface TaskPresentationOptions { /** diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index c599b3cd14..467bf5f97f 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -5,7 +5,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; -import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem } from 'vs/workbench/common/views'; +import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem, ITreeViewDragAndDropController, ITreeDataTransfer } from 'vs/workbench/common/views'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -14,6 +14,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IConnectionTreeService } from 'sql/workbench/services/connection/common/connectionTreeService'; +import { TreeDataTransferConverter } from 'vs/workbench/api/common/shared/treeDataTransfer'; @extHostNamedCustomer(MainContext.MainThreadTreeViews) export class MainThreadTreeViews extends Disposable implements MainThreadTreeViewsShape { @@ -33,18 +34,20 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews); } - async $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean }): Promise { + async $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean, canDragAndDrop: boolean }): Promise { this.logService.trace('MainThreadTreeViews#$registerTreeViewDataProvider', treeViewId, options); this.extensionService.whenInstalledExtensionsRegistered().then(() => { const dataProvider = new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService); this._dataProviders.set(treeViewId, dataProvider); + const dndController = options.canDragAndDrop ? new TreeViewDragAndDropController(treeViewId, this._proxy) : undefined; const viewer = this.getTreeView(treeViewId); if (viewer) { // Order is important here. The internal tree isn't created until the dataProvider is set. // Set all other properties first! viewer.showCollapseAllAction = !!options.showCollapseAll; viewer.canSelectMany = !!options.canSelectMany; + viewer.dragAndDropController = dndController; viewer.dataProvider = dataProvider; this.registerListeners(treeViewId, viewer); this._proxy.$setVisible(treeViewId, viewer.visible); @@ -171,6 +174,16 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie // {{SQL CARBON EDIT}} export type TreeItemHandle = string; +class TreeViewDragAndDropController implements ITreeViewDragAndDropController { + + constructor(private readonly treeViewId: string, + private readonly _proxy: ExtHostTreeViewsShape) { } + + async onDrop(dataTransfer: ITreeDataTransfer, targetTreeItem: ITreeItem): Promise { + return this._proxy.$onDrop(this.treeViewId, await TreeDataTransferConverter.toTreeDataTransferDTO(dataTransfer), targetTreeItem.handle); + } +} + // {{SQL CARBON EDIT}} export class TreeViewDataProvider implements ITreeViewDataProvider { @@ -195,7 +208,7 @@ export class TreeViewDataProvider implements ITreeViewDataProvider { })); } - getItemsToRefresh(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem; }): ITreeItem[] { + getItemsToRefresh(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem }): ITreeItem[] { const itemsToRefresh: ITreeItem[] = []; if (itemsToRefreshByHandle) { for (const treeItemHandle of Object.keys(itemsToRefreshByHandle)) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 13b1c47adc..538f174e32 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -37,6 +37,7 @@ import * as statusbar from 'vs/workbench/services/statusbar/common/statusbar'; import { ClassifiedEvent, GDPRClassification, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { ITelemetryInfo } from 'vs/platform/telemetry/common/telemetry'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { TreeDataTransferDTO } from 'vs/workbench/api/common/shared/treeDataTransfer'; import * as tasks from 'vs/workbench/api/common/shared/tasks'; import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; @@ -293,7 +294,7 @@ export interface MainThreadTextEditorsShape extends IDisposable { } export interface MainThreadTreeViewsShape extends IDisposable { - $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean; }): Promise; + $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean, canDragAndDrop: boolean; }): Promise; $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem; }): Promise; $reveal(treeViewId: string, itemInfo: { item: ITreeItem, parentChain: ITreeItem[] } | undefined, options: IRevealOptions): Promise; $setMessage(treeViewId: string, message: string): void; @@ -1234,6 +1235,7 @@ export interface ExtHostDocumentsAndEditorsShape { export interface ExtHostTreeViewsShape { // {{SQL CARBON EDIT}} $getChildren(treeViewId: string, treeItemHandle?: string): Promise; + $onDrop(treeViewId: string, treeDataTransfer: TreeDataTransferDTO, newParentTreeItemHandle: string): Promise; $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void; $setSelection(treeViewId: string, treeItemHandles: string[]): void; $setVisible(treeViewId: string, visible: boolean): void; diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index cb03f6b41c..62841dd2c2 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ExtHostTreeViewsShape, MainThreadTreeViewsShape } from './extHost.protocol'; -import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions } from 'vs/workbench/common/views'; +import { ITreeItem, TreeViewItemHandleArg, ITreeItemLabel, IRevealOptions, TREE_ITEM_DATA_TRANSFER_TYPE } from 'vs/workbench/common/views'; import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; import { asPromise } from 'vs/base/common/async'; import { TreeItemCollapsibleState, ThemeIcon, MarkdownString as MarkdownStringType } from 'vs/workbench/api/common/extHostTypes'; @@ -22,6 +22,7 @@ import { MarkdownString } from 'vs/workbench/api/common/extHostTypeConverters'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Command } from 'vs/editor/common/modes'; +import { TreeDataTransferConverter, TreeDataTransferDTO } from 'vs/workbench/api/common/shared/treeDataTransfer'; // {{SQL CARBON EDIT}} import * as azdata from 'azdata'; @@ -86,7 +87,8 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { if (!options || !options.treeDataProvider) { throw new Error('Options with treeDataProvider is mandatory'); } - const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany }); + const canDragAndDrop = options.dragAndDropController !== undefined; + const registerPromise = this._proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany, canDragAndDrop: canDragAndDrop }); const treeView = this.createExtHostTreeView(viewId, options, extension); return { get onDidCollapseElement() { return treeView.onDidCollapseElement; }, @@ -129,6 +131,27 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { return treeView.getChildren(treeItemHandle); } + async $onDrop(treeViewId: string, treeDataTransferDTO: TreeDataTransferDTO, newParentItemHandle: string): Promise { + const treeView = this.treeViews.get(treeViewId); + if (!treeView) { + return Promise.reject(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId))); + } + + const treeDataTransfer = TreeDataTransferConverter.toITreeDataTransfer(treeDataTransferDTO); + if (treeDataTransfer.items.has(TREE_ITEM_DATA_TRANSFER_TYPE)) { + const sourceHandles: string[] = JSON.parse(await treeDataTransfer.items.get(TREE_ITEM_DATA_TRANSFER_TYPE)!.asString()); + const sourceElements = sourceHandles.map(handle => treeView.getExtensionElement(handle)).filter(element => !!element); + if (sourceElements.length > 0) { + treeDataTransfer.items.set(TREE_ITEM_DATA_TRANSFER_TYPE, { + asString: async () => JSON.stringify(sourceElements) + }); + } else { + treeDataTransfer.items.delete(TREE_ITEM_DATA_TRANSFER_TYPE); + } + } + return treeView.onDrop(treeDataTransfer, newParentItemHandle); + } + async $hasResolve(treeViewId: string): Promise { const treeView = this.treeViews.get(treeViewId); if (!treeView) { @@ -199,6 +222,7 @@ export class ExtHostTreeView extends Disposable { private static readonly ID_HANDLE_PREFIX = '1'; private readonly dataProvider: vscode.TreeDataProvider; + private readonly dndController: vscode.DragAndDropController | undefined; private roots: TreeNode[] | null = null; private elements: Map = new Map(); @@ -246,9 +270,11 @@ export class ExtHostTreeView extends Disposable { } } this.dataProvider = options.treeDataProvider; + this.dndController = options.dragAndDropController; + // {{SQL CARBON EDIT}} if (this.proxy) { - this.proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany }); + this.proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany, canDragAndDrop: options.dragAndDropController !== undefined }); } if (this.dataProvider.onDidChangeTreeData) { this._register(this.dataProvider.onDidChangeTreeData(element => this._onDidChangeData.fire({ message: false, element }))); @@ -377,6 +403,14 @@ export class ExtHostTreeView extends Disposable { } } + async onDrop(treeDataTransfer: vscode.TreeDataTransfer, targetHandleOrNode: TreeItemHandle): Promise { + const target = this.getExtensionElement(targetHandleOrNode); + if (!target) { + return; + } + return asPromise(() => this.dndController?.onDrop(treeDataTransfer, target)); + } + get hasResolve(): boolean { return !!this.dataProvider.resolveTreeItem; } diff --git a/src/vs/workbench/api/common/shared/treeDataTransfer.ts b/src/vs/workbench/api/common/shared/treeDataTransfer.ts new file mode 100644 index 0000000000..123d05adee --- /dev/null +++ b/src/vs/workbench/api/common/shared/treeDataTransfer.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITreeDataTransfer, ITreeDataTransferItem } from 'vs/workbench/common/views'; + +interface TreeDataTransferItemDTO { + asString: string; +} + +export interface TreeDataTransferDTO { + types: string[]; + items: TreeDataTransferItemDTO[]; +} + +export namespace TreeDataTransferConverter { + export function toITreeDataTransfer(value: TreeDataTransferDTO): ITreeDataTransfer { + const newDataTransfer: ITreeDataTransfer = { + items: new Map() + }; + value.types.forEach((type, index) => { + newDataTransfer.items.set(type, { + asString: async () => value.items[index].asString + }); + }); + return newDataTransfer; + } + + export async function toTreeDataTransferDTO(value: ITreeDataTransfer): Promise { + const newDTO: TreeDataTransferDTO = { + types: [], + items: [] + }; + const entries = Array.from(value.items.entries()); + for (const entry of entries) { + newDTO.types.push(entry[0]); + newDTO.items.push({ + asString: await entry[1].asString() + }); + } + return newDTO; + } +} diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index b566994065..ccfe597a7b 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -10,7 +10,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { MenuId, IMenuService, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem } from 'vs/workbench/common/views'; +import { ITreeView, ITreeViewDescriptor, IViewsRegistry, Extensions, IViewDescriptorService, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeItemLabel, ViewContainer, ViewContainerLocation, ResolvableTreeItem, ITreeViewDragAndDropController, ITreeDataTransfer, TREE_ITEM_DATA_TRANSFER_TYPE } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IThemeService, FileThemeIcon, FolderThemeIcon, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -38,7 +38,8 @@ import { textLinkForeground, textCodeBlockBackground, focusBorder, listFilterMat import { isString } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, TreeDragOverBubble } from 'vs/base/browser/ui/tree/tree'; +import { IDragAndDropData } from 'vs/base/browser/dnd'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { CollapseAllAction } from 'vs/base/browser/ui/tree/treeDefaults'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; @@ -55,6 +56,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Command } from 'vs/editor/common/modes'; import { isPromiseCanceledError } from 'vs/base/common/errors'; +import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; export class TreeViewPane extends ViewPane { @@ -87,6 +89,7 @@ export class TreeViewPane extends ViewPane { if (options.titleDescription !== this.treeView.description) { this.updateTitleDescription(this.treeView.description); } + this.updateTreeVisibility(); } @@ -237,6 +240,13 @@ export class TreeView extends Disposable implements ITreeView { get viewLocation(): ViewContainerLocation { return this.viewDescriptorService.getViewLocationById(this.id)!; } + private _dragAndDropController: ITreeViewDragAndDropController | undefined; + get dragAndDropController(): ITreeViewDragAndDropController | undefined { + return this._dragAndDropController; + } + set dragAndDropController(dnd: ITreeViewDragAndDropController | undefined) { + this._dragAndDropController = dnd; + } private _dataProvider: ITreeViewDataProvider | undefined; get dataProvider(): ITreeViewDataProvider | undefined { @@ -503,6 +513,7 @@ export class TreeView extends Disposable implements ITreeView { return e.collapsibleState !== TreeItemCollapsibleState.Expanded; }, multipleSelectionSupport: this.canSelectMany, + dnd: this.dragAndDropController ? this.instantiationService.createInstance(CustomTreeViewDragAndDrop, this.dragAndDropController) : undefined, overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } @@ -1183,3 +1194,65 @@ export class CustomTreeView extends TreeView { } } } + +export class CustomTreeViewDragAndDrop implements ITreeDragAndDrop { + constructor(private dndController: ITreeViewDragAndDropController, @ILabelService private readonly labelService: ILabelService) { } + + onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { + if (originalEvent.dataTransfer) { + originalEvent.dataTransfer.setData(TREE_ITEM_DATA_TRANSFER_TYPE, + JSON.stringify((data as ElementsDragAndDropData).getData().map(treeItem => treeItem.handle))); + } + } + + onDragOver(data: IDragAndDropData, targetElement: ITreeItem, targetIndex: number, originalEvent: DragEvent): boolean | ITreeDragOverReaction { + return { accept: true, bubble: TreeDragOverBubble.Down, autoExpand: true }; + } + + getDragURI(element: ITreeItem): string | null { + return element.resourceUri ? URI.revive(element.resourceUri).toString() : element.handle; + } + + getDragLabel?(elements: ITreeItem[]): string | undefined { + if (elements.length > 1) { + return String(elements.length); + } + const element = elements[0]; + return element.label ? element.label.label : (element.resourceUri ? this.labelService.getUriLabel(URI.revive(element.resourceUri)) : undefined); + } + + async drop(data: IDragAndDropData, targetNode: ITreeItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): Promise { + if (!originalEvent.dataTransfer || !this.dndController || !targetNode) { + return; + } + const treeDataTransfer: ITreeDataTransfer = { + items: new Map() + }; + let stringCount = Array.from(originalEvent.dataTransfer.items).reduce((previous, current) => { + if (current.kind === 'string') { + return previous + 1; + } + return previous; + }, 0); + await new Promise(resolve => { + if (!originalEvent.dataTransfer || !this.dndController || !targetNode) { + return; + } + for (const dataItem of originalEvent.dataTransfer.items) { + if (dataItem.kind === 'string') { + const type = dataItem.type; + dataItem.getAsString(dataValue => { + treeDataTransfer.items.set(type, { + asString: () => Promise.resolve(dataValue) + }); + stringCount--; + if (stringCount === 0) { + resolve(); + } + }); + } + } + }); + return this.dndController.onDrop(treeDataTransfer, targetNode); + } +} diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 468500b59c..c9f2e6a728 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -628,10 +628,20 @@ export interface IViewDescriptorService { // Custom views +export interface ITreeDataTransferItem { + asString(): Thenable; +} + +export interface ITreeDataTransfer { + items: Map; +} + export interface ITreeView extends IDisposable { dataProvider: ITreeViewDataProvider | undefined; + dragAndDropController?: ITreeViewDragAndDropController; + showCollapseAllAction: boolean; canSelectMany: boolean; @@ -814,7 +824,11 @@ export interface ITreeViewDataProvider { readonly isTreeEmpty?: boolean; onDidChangeEmpty?: Event; getChildren(element?: ITreeItem): Promise; +} +export const TREE_ITEM_DATA_TRANSFER_TYPE = 'text/treeitems'; +export interface ITreeViewDragAndDropController { + onDrop(elements: ITreeDataTransfer, target: ITreeItem): Promise; } export interface IEditableData { diff --git a/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts b/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts index 28ef23a58f..904b70e44f 100644 --- a/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadTreeViews.test.ts @@ -70,7 +70,7 @@ suite('MainThreadHostTreeView', function () { } drain(): any { return null; } }, new TestViewsService(), new TestNotificationService(), testExtensionService, new NullLogService(), undefined!); - mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false }); + mainThreadTreeViews.$registerTreeViewDataProvider(testTreeViewId, { showCollapseAll: false, canSelectMany: false, canDragAndDrop: false }); await testExtensionService.whenInstalledExtensionsRegistered(); });