Skip to content

Commit cdea3dc

Browse files
authored
Multi-select in custom tree view (microsoft#78625)
Part of microsoft#76941 The first argument is now the element that the command is executed on. The second argurment is an array of the other selected items
1 parent 7c53175 commit cdea3dc

6 files changed

Lines changed: 67 additions & 23 deletions

File tree

src/vs/vscode.proposed.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,15 @@ declare module 'vscode' {
10461046
*/
10471047
constructor(label: TreeItemLabel, collapsibleState?: TreeItemCollapsibleState);
10481048
}
1049+
1050+
export interface TreeViewOptions2<T> extends TreeViewOptions<T> {
1051+
/**
1052+
* Whether the tree supports multi-select. When the tree supports multi-select and a command is executed from the tree,
1053+
* the first argument to the command is the tree item that the command was executed on and the second argument is an
1054+
* array containing the other selected tree items.
1055+
*/
1056+
canSelectMany?: boolean;
1057+
}
10491058
//#endregion
10501059

10511060
//#region CustomExecution

src/vs/workbench/api/browser/mainThreadTreeViews.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie
2727
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews);
2828
}
2929

30-
$registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean }): void {
30+
$registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean }): void {
3131
const dataProvider = new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService);
3232
this._dataProviders.set(treeViewId, dataProvider);
3333
const viewer = this.getTreeView(treeViewId);
3434
if (viewer) {
3535
viewer.dataProvider = dataProvider;
3636
viewer.showCollapseAllAction = !!options.showCollapseAll;
37+
viewer.canSelectMany = !!options.canSelectMany;
3738
this.registerListeners(treeViewId, viewer);
3839
this._proxy.$setVisible(treeViewId, viewer.visible);
3940
} else {

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ export interface MainThreadTextEditorsShape extends IDisposable {
242242
}
243243

244244
export interface MainThreadTreeViewsShape extends IDisposable {
245-
$registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean }): void;
245+
$registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean }): void;
246246
$refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Promise<void>;
247247
$reveal(treeViewId: string, treeItem: ITreeItem, parentChain: ITreeItem[], options: IRevealOptions): Promise<void>;
248248
$setMessage(treeViewId: string, message: string): void;

src/vs/workbench/api/common/extHostTreeViews.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,21 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
5252
private commands: ExtHostCommands,
5353
private logService: ILogService
5454
) {
55+
56+
function isTreeViewItemHandleArg(arg: any): boolean {
57+
return arg && arg.$treeViewId && arg.$treeItemHandle;
58+
}
5559
commands.registerArgumentProcessor({
5660
processArgument: arg => {
57-
if (arg && arg.$treeViewId && arg.$treeItemHandle) {
61+
if (isTreeViewItemHandleArg(arg)) {
5862
return this.convertArgument(arg);
63+
} else if (Array.isArray(arg) && (arg.length > 0)) {
64+
return arg.map(item => {
65+
if (isTreeViewItemHandleArg(item)) {
66+
return this.convertArgument(item);
67+
}
68+
return item;
69+
});
5970
}
6071
return arg;
6172
}
@@ -182,10 +193,10 @@ class ExtHostTreeView<T> extends Disposable {
182193
private refreshPromise: Promise<void> = Promise.resolve();
183194
private refreshQueue: Promise<void> = Promise.resolve();
184195

185-
constructor(private viewId: string, options: vscode.TreeViewOptions<T>, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter, private logService: ILogService, private extension: IExtensionDescription) {
196+
constructor(private viewId: string, options: vscode.TreeViewOptions2<T>, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter, private logService: ILogService, private extension: IExtensionDescription) {
186197
super();
187198
this.dataProvider = options.treeDataProvider;
188-
this.proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll });
199+
this.proxy.$registerTreeViewDataProvider(viewId, { showCollapseAll: !!options.showCollapseAll, canSelectMany: !!options.canSelectMany });
189200
if (this.dataProvider.onDidChangeTreeData) {
190201
this._register(this.dataProvider.onDidChangeTreeData(element => this._onDidChangeData.fire({ message: false, element })));
191202
}

src/vs/workbench/browser/parts/views/customView.ts

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,11 @@ export class CustomTreeView extends Disposable implements ITreeView {
164164
private domNode!: HTMLElement;
165165
private treeContainer!: HTMLElement;
166166
private _messageValue: string | undefined;
167+
private _canSelectMany: boolean = false;
167168
private messageElement!: HTMLDivElement;
168169
private tree: WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore> | undefined;
169170
private treeLabels: ResourceLabels | undefined;
171+
170172
private root: ITreeItem;
171173
private elementsToRefresh: ITreeItem[] = [];
172174
private menus: TitleMenus;
@@ -253,6 +255,14 @@ export class CustomTreeView extends Disposable implements ITreeView {
253255
this.updateMessage();
254256
}
255257

258+
get canSelectMany(): boolean {
259+
return this._canSelectMany;
260+
}
261+
262+
set canSelectMany(canSelectMany: boolean) {
263+
this._canSelectMany = canSelectMany;
264+
}
265+
256266
get hasIconForParentNode(): boolean {
257267
return this._hasIconForParentNode;
258268
}
@@ -372,12 +382,14 @@ export class CustomTreeView extends Disposable implements ITreeView {
372382
collapseByDefault: (e: ITreeItem): boolean => {
373383
return e.collapsibleState !== TreeItemCollapsibleState.Expanded;
374384
},
375-
multipleSelectionSupport: false
376-
}));
385+
multipleSelectionSupport: this.canSelectMany,
386+
}) as WorkbenchAsyncDataTree<ITreeItem, ITreeItem, FuzzyScore>);
377387
aligner.tree = this.tree;
388+
const actionRunner = new MultipleSelectionActionRunner(() => this.tree!.getSelection());
389+
renderer.actionRunner = actionRunner;
378390

379391
this.tree.contextKeyService.createKey<boolean>(this.id, true);
380-
this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e)));
392+
this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner)));
381393
this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements)));
382394
this._register(this.tree.onDidChangeCollapseState(e => {
383395
if (!e.node.element) {
@@ -406,7 +418,7 @@ export class CustomTreeView extends Disposable implements ITreeView {
406418
}));
407419
}
408420

409-
private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent<ITreeItem>): void {
421+
private onContextMenu(treeMenus: TreeMenus, treeEvent: ITreeContextMenuEvent<ITreeItem>, actionRunner: MultipleSelectionActionRunner): void {
410422
const node: ITreeItem | null = treeEvent.element;
411423
if (node === null) {
412424
return;
@@ -442,7 +454,7 @@ export class CustomTreeView extends Disposable implements ITreeView {
442454

443455
getActionsContext: () => (<TreeViewItemHandleArg>{ $treeViewId: this.id, $treeItemHandle: node.handle }),
444456

445-
actionRunner: new MultipleSelectionActionRunner(() => this.tree!.getSelection())
457+
actionRunner
446458
});
447459
}
448460

@@ -686,6 +698,8 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
686698
static readonly ITEM_HEIGHT = 22;
687699
static readonly TREE_TEMPLATE_ID = 'treeExplorer';
688700

701+
private _actionRunner: MultipleSelectionActionRunner | undefined;
702+
689703
constructor(
690704
private treeViewId: string,
691705
private menus: TreeMenus,
@@ -703,6 +717,10 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
703717
return TreeRenderer.TREE_TEMPLATE_ID;
704718
}
705719

720+
set actionRunner(actionRunner: MultipleSelectionActionRunner) {
721+
this._actionRunner = actionRunner;
722+
}
723+
706724
renderTemplate(container: HTMLElement): ITreeExplorerTemplateData {
707725
DOM.addClass(container, 'custom-view-tree-node-item');
708726

@@ -740,8 +758,11 @@ class TreeRenderer extends Disposable implements ITreeRenderer<ITreeItem, FuzzyS
740758

741759
templateData.icon.style.backgroundImage = iconUrl ? `url('${DOM.asDomUri(iconUrl).toString(true)}')` : '';
742760
DOM.toggleClass(templateData.icon, 'custom-view-tree-node-item-icon', !!iconUrl);
743-
templateData.actionBar.context = (<TreeViewItemHandleArg>{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle });
761+
templateData.actionBar.context = <TreeViewItemHandleArg>{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle };
744762
templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false });
763+
if (this._actionRunner) {
764+
templateData.actionBar.actionRunner = this._actionRunner;
765+
}
745766
this.setAlignment(templateData.container, node);
746767
templateData.elementDisposable = (this.themeService.onDidFileIconThemeChange(() => this.setAlignment(templateData.container, node)));
747768
}
@@ -822,23 +843,23 @@ class Aligner extends Disposable {
822843

823844
class MultipleSelectionActionRunner extends ActionRunner {
824845

825-
constructor(private getSelectedResources: (() => any[])) {
846+
constructor(private getSelectedResources: (() => ITreeItem[])) {
826847
super();
827848
}
828849

829-
runAction(action: IAction, context: any): Promise<any> {
830-
if (action instanceof MenuItemAction) {
831-
const selection = this.getSelectedResources();
832-
const filteredSelection = selection.filter(s => s !== context);
833-
834-
if (selection.length === filteredSelection.length || selection.length === 1) {
835-
return action.run(context);
836-
}
837-
838-
return action.run(context, ...filteredSelection);
850+
runAction(action: IAction, context: TreeViewItemHandleArg): Promise<any> {
851+
const selection = this.getSelectedResources();
852+
let selectionHandleArgs: TreeViewItemHandleArg[] | undefined = undefined;
853+
if (selection.length > 1) {
854+
selectionHandleArgs = [];
855+
selection.forEach(selected => {
856+
if (selected.handle !== context.$treeItemHandle) {
857+
selectionHandleArgs!.push({ $treeViewId: context.$treeViewId, $treeItemHandle: selected.handle });
858+
}
859+
});
839860
}
840861

841-
return super.runAction(action, context);
862+
return action.run(...[context, selectionHandleArgs]);
842863
}
843864
}
844865

src/vs/workbench/common/views.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ export interface ITreeView extends IDisposable {
310310

311311
showCollapseAllAction: boolean;
312312

313+
canSelectMany: boolean;
314+
313315
message?: string;
314316

315317
readonly visible: boolean;

0 commit comments

Comments
 (0)