From 6a152ca5231c2035bc8718e41dc96047e33bed96 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Fri, 14 Jul 2023 08:49:15 +0000 Subject: [PATCH] Expose the focused element and change event in the TreeView API (#184268) * Expose the focused element and event in the TreeView API * Exposed active TreeItem through extension proposal * Add proposal to test extension * Merge change selection and focus events * Finish selection+focus change in treeview * Clean up * Clean up * Add checkProposedApiEnabled back in --------- Co-authored-by: Ehab Younes Co-authored-by: Alex Ross --- extensions/vscode-api-tests/package.json | 1 + .../api/browser/mainThreadTreeViews.ts | 3 +- .../workbench/api/common/extHost.protocol.ts | 3 +- .../workbench/api/common/extHostTreeViews.ts | 41 +++++++++++-------- .../workbench/browser/parts/views/treeView.ts | 26 ++++++++---- src/vs/workbench/common/views.ts | 4 +- .../common/extensionsApiProposals.ts | 1 + .../vscode.proposed.treeViewActiveItem.d.ts | 30 ++++++++++++++ 8 files changed, 77 insertions(+), 32 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index b230a1a4897..369927c00f2 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -44,6 +44,7 @@ "timeline", "tokenInformation", "treeItemCheckbox", + "treeViewActiveItem", "treeViewReveal", "testInvalidateResults", "workspaceTrust", diff --git a/src/vs/workbench/api/browser/mainThreadTreeViews.ts b/src/vs/workbench/api/browser/mainThreadTreeViews.ts index 6b6a75aaf60..04170a3c9a0 100644 --- a/src/vs/workbench/api/browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/browser/mainThreadTreeViews.ts @@ -177,8 +177,7 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie private registerListeners(treeViewId: string, treeView: ITreeView): void { this._register(treeView.onDidExpandItem(item => this._proxy.$setExpanded(treeViewId, item.handle, true))); this._register(treeView.onDidCollapseItem(item => this._proxy.$setExpanded(treeViewId, item.handle, false))); - this._register(treeView.onDidChangeSelection(items => this._proxy.$setSelection(treeViewId, items.map(({ handle }) => handle)))); - this._register(treeView.onDidChangeFocus(item => this._proxy.$setFocus(treeViewId, item.handle))); + this._register(treeView.onDidChangeSelectionAndFocus(items => this._proxy.$setSelectionAndFocus(treeViewId, items.selection.map(({ handle }) => handle), items.focus.handle))); this._register(treeView.onDidChangeVisibility(isVisible => this._proxy.$setVisible(treeViewId, isVisible))); this._register(treeView.onDidChangeCheckboxState(items => { this._proxy.$changeCheckboxState(treeViewId, items.map(item => { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 3471b4cecaf..5320cb4a8b0 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1563,8 +1563,7 @@ export interface ExtHostTreeViewsShape { $handleDrop(destinationViewId: string, requestId: number, treeDataTransfer: DataTransferDTO, targetHandle: string | undefined, token: CancellationToken, operationUuid?: string, sourceViewId?: string, sourceTreeItemHandles?: string[]): Promise; $handleDrag(sourceViewId: string, sourceTreeItemHandles: string[], operationUuid: string, token: CancellationToken): Promise; $setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void; - $setSelection(treeViewId: string, treeItemHandles: string[]): void; - $setFocus(treeViewId: string, treeItemHandle: string): void; + $setSelectionAndFocus(treeViewId: string, selectionHandles: string[], focusHandle: string): void; $setVisible(treeViewId: string, visible: boolean): void; $changeCheckboxState(treeViewId: string, checkboxUpdates: CheckboxUpdate[]): void; $hasResolve(treeViewId: string): Promise; diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 2950a3b5894..3aaa6a38b29 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -24,6 +24,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { ITreeViewsDnDService, TreeViewsDnDService } from 'vs/editor/common/services/treeViewsDnd'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; type TreeItemHandle = string; @@ -99,6 +100,14 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { get onDidExpandElement() { return treeView.onDidExpandElement; }, get selection() { return treeView.selectedElements; }, get onDidChangeSelection() { return treeView.onDidChangeSelection; }, + get activeItem() { + checkProposedApiEnabled(extension, 'treeViewActiveItem'); + return treeView.focusedElement; + }, + get onDidChangeActiveItem() { + checkProposedApiEnabled(extension, 'treeViewActiveItem'); + return treeView.onDidChangeActiveItem; + }, get visible() { return treeView.visible; }, get onDidChangeVisibility() { return treeView.onDidChangeVisibility; }, get onDidChangeCheckboxState() { @@ -222,20 +231,12 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { treeView.setExpanded(treeItemHandle, expanded); } - $setSelection(treeViewId: string, treeItemHandles: string[]): void { + $setSelectionAndFocus(treeViewId: string, selectedHandles: string[], focusedHandle: string) { const treeView = this.treeViews.get(treeViewId); if (!treeView) { throw new NoTreeViewError(treeViewId); } - treeView.setSelection(treeItemHandles); - } - - $setFocus(treeViewId: string, treeItemHandles: string) { - const treeView = this.treeViews.get(treeViewId); - if (!treeView) { - throw new NoTreeViewError(treeViewId); - } - treeView.setFocus(treeItemHandles); + treeView.setSelectionAndFocus(selectedHandles, focusedHandle); } $setVisible(treeViewId: string, isVisible: boolean): void { @@ -313,6 +314,9 @@ class ExtHostTreeView extends Disposable { private _onDidChangeSelection: Emitter> = this._register(new Emitter>()); readonly onDidChangeSelection: Event> = this._onDidChangeSelection.event; + private _onDidChangeActiveItem: Emitter> = this._register(new Emitter>()); + readonly onDidChangeActiveItem: Event> = this._onDidChangeActiveItem.event; + private _onDidChangeVisibility: Emitter = this._register(new Emitter()); readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; @@ -479,15 +483,20 @@ class ExtHostTreeView extends Disposable { } } - setSelection(treeItemHandles: TreeItemHandle[]): void { - if (!equals(this._selectedHandles, treeItemHandles)) { - this._selectedHandles = treeItemHandles; + setSelectionAndFocus(selectedHandles: TreeItemHandle[], focusedHandle: string): void { + const changedSelection = !equals(this._selectedHandles, selectedHandles); + this._selectedHandles = selectedHandles; + + const changedFocus = this._focusedHandle !== focusedHandle; + this._focusedHandle = focusedHandle; + + if (changedSelection) { this._onDidChangeSelection.fire(Object.freeze({ selection: this.selectedElements })); } - } - setFocus(treeItemHandle: TreeItemHandle) { - this._focusedHandle = treeItemHandle; + if (changedFocus) { + this._onDidChangeActiveItem.fire(Object.freeze({ activeItem: this.focusedElement })); + } } setVisible(visible: boolean): void { diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 5cacc80e6ba..77b9e62a7b7 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -215,6 +215,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private root: ITreeItem; private elementsToRefresh: ITreeItem[] = []; + private lastSelection: readonly ITreeItem[] = []; + private lastActive: ITreeItem; private readonly _onDidExpandItem: Emitter = this._register(new Emitter()); readonly onDidExpandItem: Event = this._onDidExpandItem.event; @@ -222,11 +224,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { private readonly _onDidCollapseItem: Emitter = this._register(new Emitter()); readonly onDidCollapseItem: Event = this._onDidCollapseItem.event; - private _onDidChangeSelection: Emitter = this._register(new Emitter()); - readonly onDidChangeSelection: Event = this._onDidChangeSelection.event; - - private _onDidChangeFocus: Emitter = this._register(new Emitter()); - readonly onDidChangeFocus: Event = this._onDidChangeFocus.event; + private _onDidChangeSelectionAndFocus: Emitter<{ selection: readonly ITreeItem[]; focus: ITreeItem }> = this._register(new Emitter<{ selection: readonly ITreeItem[]; focus: ITreeItem }>()); + readonly onDidChangeSelectionAndFocus: Event<{ selection: readonly ITreeItem[]; focus: ITreeItem }> = this._onDidChangeSelectionAndFocus.event; private readonly _onDidChangeVisibility: Emitter = this._register(new Emitter()); readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; @@ -267,6 +266,7 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { ) { super(); this.root = new Root(); + this.lastActive = this.root; // Try not to add anything that could be costly to this constructor. It gets called once per tree view // during startup, and anything added here can affect performance. } @@ -702,10 +702,17 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { const customTreeKey = RawCustomTreeViewContextKey.bindTo(this.tree.contextKeyService); customTreeKey.set(true); this._register(this.tree.onContextMenu(e => this.onContextMenu(treeMenus, e, actionRunner))); - this._register(this.tree.onDidChangeSelection(e => this._onDidChangeSelection.fire(e.elements))); + + this._register(this.tree.onDidChangeSelection(e => { + this.lastSelection = e.elements; + this.lastActive = this.tree?.getFocus()[0] ?? this.lastActive; + this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive }); + })); this._register(this.tree.onDidChangeFocus(e => { - if (e.elements.length) { - this._onDidChangeFocus.fire(e.elements[0]); + if (e.elements.length && (e.elements[0] !== this.lastActive)) { + this.lastActive = e.elements[0]; + this.lastSelection = this.tree?.getSelection() ?? this.lastSelection; + this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive }); } })); this._register(this.tree.onDidChangeCollapseState(e => { @@ -957,7 +964,8 @@ abstract class AbstractTreeView extends Disposable implements ITreeView { } const newSelection = tree.getSelection(); if (oldSelection.length !== newSelection.length || oldSelection.some((value, index) => value.handle !== newSelection[index].handle)) { - this._onDidChangeSelection.fire(newSelection); + this.lastSelection = newSelection; + this._onDidChangeSelectionAndFocus.fire({ selection: this.lastSelection, focus: this.lastActive }); } this.refreshing = false; this._onDidCompleteRefresh.fire(); diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 99efb9f3133..f86f9cb92bb 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -667,9 +667,7 @@ export interface ITreeView extends IDisposable { readonly onDidCollapseItem: Event; - readonly onDidChangeSelection: Event; - - readonly onDidChangeFocus: Event; + readonly onDidChangeSelectionAndFocus: Event<{ selection: readonly ITreeItem[]; focus: ITreeItem }>; readonly onDidChangeVisibility: Event; diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index ed18b53ce4f..98f89dca708 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -91,6 +91,7 @@ export const allApiProposals = Object.freeze({ textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts', timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts', tokenInformation: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tokenInformation.d.ts', + treeViewActiveItem: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts', treeViewReveal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.treeViewReveal.d.ts', tunnels: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.tunnels.d.ts', windowActivity: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.windowActivity.d.ts', diff --git a/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts b/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts new file mode 100644 index 00000000000..27eab8603f6 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.treeViewActiveItem.d.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/170248 + + export interface TreeView extends Disposable { + /** + * Currently active item. + */ + readonly activeItem: T | undefined; + /** + * Event that is fired when the {@link TreeView.activeItem active item} has changed + */ + readonly onDidChangeActiveItem: Event>; + } + + /** + * The event that is fired when there is a change in {@link TreeView.activeItem tree view's active item} + */ + export interface TreeViewActiveItemChangeEvent { + /** + * Active item. + */ + readonly activeItem: T | undefined; + } +}