From ba91a6a9b1bf6714424ac2274a999cf3c0543b84 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 18 Oct 2023 21:31:37 +0200 Subject: [PATCH 01/26] Basic functionality is working. More work to do. --- .../contrib/scm/browser/scmViewPane.ts | 262 +++++++++++++----- 1 file changed, 185 insertions(+), 77 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 46151c7881d58..43a76e400708a 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -24,22 +24,22 @@ import { IAction, ActionRunner, Action, Separator } from 'vs/base/common/actions import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton } from './util'; -import { WorkbenchCompressibleObjectTree, IOpenEvent } from 'vs/platform/list/browser/listService'; +import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, ThrottledDelayer } from 'vs/base/common/async'; -import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction } from 'vs/base/browser/ui/tree/tree'; +import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { ResourceTree, IResourceNode } from 'vs/base/common/resourceTree'; import { ISplice } from 'vs/base/common/sequence'; import { ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/tree/objectTree'; import { Iterable } from 'vs/base/common/iterator'; -import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { URI } from 'vs/base/common/uri'; import { FileKind } from 'vs/platform/files/common/files'; import { compareFileNames, comparePaths } from 'vs/base/common/comparers'; import { FuzzyScore, createMatches, IMatch } from 'vs/base/common/filters'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; -import { coalesce, flatten } from 'vs/base/common/arrays'; +import { flatten } from 'vs/base/common/arrays'; import { memoize } from 'vs/base/common/decorators'; import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; @@ -96,6 +96,7 @@ import { fillEditorsDragData } from 'vs/workbench/browser/dnd'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { CodeDataTransfers } from 'vs/platform/dnd/browser/dnd'; import { FormatOnType } from 'vs/editor/contrib/format/browser/formatActions'; +import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | IResourceNode | ISCMResource; @@ -503,7 +504,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer 0 ? FileKind.FOLDER : FileKind.FILE; const viewModel = this.viewModelProvider(); const tooltip = !ResourceTree.isResourceNode(resourceOrFolder) && resourceOrFolder.decorations.tooltip || ''; @@ -728,6 +729,18 @@ class ListDelegate implements IListVirtualDelegate { } } +class SCMTreeCompressionDelegate implements ITreeCompressionDelegate { + + isIncompressible(element: TreeElement): boolean { + if (ResourceTree.isResourceNode(element)) { + return element.childrenCount === 0 || !element.parent || !element.parent.parent; + } + + return true; + } + +} + class SCMTreeFilter implements ITreeFilter { filter(element: TreeElement): boolean { @@ -960,18 +973,18 @@ function isRepositoryItem(item: IRepositoryItem | IGroupItem): item is IReposito return Array.isArray((item as IRepositoryItem).groupItems); } -function asTreeElement(node: IResourceNode, forceIncompressible: boolean, viewState?: ITreeViewState): ICompressedTreeElement { - const element = (node.childrenCount === 0 && node.element) ? node.element : node; - const collapsed = viewState ? viewState.collapsed.indexOf(getSCMResourceId(element)) > -1 : false; +// function asTreeElement(node: IResourceNode, forceIncompressible: boolean, viewState?: ITreeViewState): ICompressedTreeElement { +// const element = (node.childrenCount === 0 && node.element) ? node.element : node; +// const collapsed = viewState ? viewState.collapsed.indexOf(getSCMResourceId(element)) > -1 : false; - return { - element, - children: Iterable.map(node.children, node => asTreeElement(node, false, viewState)), - incompressible: !!node.element || forceIncompressible, - collapsed, - collapsible: node.childrenCount > 0 - }; -} +// return { +// element, +// children: Iterable.map(node.children, node => asTreeElement(node, false, viewState)), +// incompressible: !!node.element || forceIncompressible, +// collapsed, +// collapsible: node.childrenCount > 0 +// }; +// } const enum ViewModelMode { List = 'list', @@ -1142,17 +1155,17 @@ class ViewModel { this._mode = mode; - for (const [, item] of this.items) { - for (const groupItem of item.groupItems) { - groupItem.tree.clear(); + // for (const [, item] of this.items) { + // for (const groupItem of item.groupItems) { + // groupItem.tree.clear(); - if (mode === ViewModelMode.Tree) { - for (const resource of groupItem.resources) { - groupItem.tree.add(resource.sourceUri, resource); - } - } - } - } + // if (mode === ViewModelMode.Tree) { + // for (const resource of groupItem.resources) { + // groupItem.tree.add(resource.sourceUri, resource); + // } + // } + // } + // } // Update sort key based on view mode this.sortKey = this.getViewModelSortKey(); @@ -1191,11 +1204,13 @@ class ViewModel { return this._treeViewState; } + private _showActionButton = false; + get showActionButton(): boolean { return this._showActionButton; } + private items = new Map(); private readonly visibilityDisposables = new DisposableStore(); private scrollTop: number | undefined; private alwaysShowRepositories = false; - private showActionButton = false; private firstVisible = true; private readonly disposables = new DisposableStore(); @@ -1212,7 +1227,7 @@ class ViewModel { private _treeViewState: ITreeViewState | undefined; constructor( - private tree: WorkbenchCompressibleObjectTree, + private tree: WorkbenchCompressibleAsyncDataTree, private inputRenderer: InputRenderer, @IInstantiationService protected instantiationService: IInstantiationService, @IEditorService protected editorService: IEditorService, @@ -1276,7 +1291,7 @@ class ViewModel { private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('scm.alwaysShowRepositories') || e.affectsConfiguration('scm.showActionButton')) { this.alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories'); - this.showActionButton = this.configurationService.getValue('scm.showActionButton'); + this._showActionButton = this.configurationService.getValue('scm.showActionButton'); this.refresh(); } } @@ -1285,10 +1300,10 @@ class ViewModel { for (const repository of added) { const disposable = combinedDisposable( repository.provider.groups.onDidSplice(splice => this._onDidSpliceGroups(item, splice)), - repository.input.onDidChangeVisibility(() => this.refresh(item)), + repository.input.onDidChangeVisibility(() => this.refresh(item.element)), repository.provider.onDidChange(() => { if (this.showActionButton) { - this.refresh(item); + this.refresh(item.element); } }) ); @@ -1360,7 +1375,7 @@ class ViewModel { if (before !== after && (before === 0 || after === 0)) { this.refresh(); } else { - this.refresh(item); + this.refresh(item.element); } } @@ -1388,7 +1403,7 @@ class ViewModel { this.updateRepositoryCollapseAllContextKeys(); } - private refresh(item?: IRepositoryItem | IGroupItem): void { + private async refresh(item?: ISCMRepository | ISCMResourceGroup): Promise { if (!this.alwaysShowRepositories && this.items.size === 1) { const provider = Iterable.first(this.items.values())!.element.provider; this.scmProviderContextKey.set(provider.contextValue); @@ -1402,16 +1417,30 @@ class ViewModel { const focusedInput = this.inputRenderer.getFocusedInput(); - if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isRepositoryItem(item)))) { - const item = Iterable.first(this.items.values())!; - this.tree.setChildren(null, this.render(item, this.treeViewState).children); + if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isSCMRepository(item)))) { + // Single repository and not always show repositories + await this.tree.setInput(Iterable.first(this.items.keys())!); } else if (item) { - this.tree.setChildren(item.element, this.render(item, this.treeViewState).children); + // Particular repository or resource group + await this.tree.updateChildren(item); } else { - const items = coalesce(this.scmViewService.visibleRepositories.map(r => this.items.get(r))); - this.tree.setChildren(null, items.map(item => this.render(item, this.treeViewState))); + // Expand repository nodes + // const expanded = Array.from(this.items.keys()) + // .map(repository => `repo:${repository.provider.id}`); + + // await this.tree.setInput([...this.items.keys()], { expanded }); } + // if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isRepositoryItem(item)))) { + // const item = Iterable.first(this.items.values())!; + // this.tree.setChildren(null, this.render(item, this.treeViewState).children); + // } else if (item) { + // this.tree.setChildren(item.element, this.render(item, this.treeViewState).children); + // } else { + // const items = coalesce(this.scmViewService.visibleRepositories.map(r => this.items.get(r))); + // this.tree.setChildren(null, items.map(item => this.render(item, this.treeViewState))); + // } + if (focusedInput) { this.inputRenderer.getRenderedInputWidget(focusedInput)?.focus(); } @@ -1419,45 +1448,45 @@ class ViewModel { this.updateRepositoryCollapseAllContextKeys(); } - private render(item: IRepositoryItem | IGroupItem, treeViewState?: ITreeViewState): ICompressedTreeElement { - if (isRepositoryItem(item)) { - const children: ICompressedTreeElement[] = []; - const hasSomeChanges = item.groupItems.some(item => item.element.elements.length > 0); + // private render(item: IRepositoryItem | IGroupItem, treeViewState?: ITreeViewState): ICompressedTreeElement { + // if (isRepositoryItem(item)) { + // const children: ICompressedTreeElement[] = []; + // const hasSomeChanges = item.groupItems.some(item => item.element.elements.length > 0); - if (item.element.input.visible) { - children.push({ element: item.element.input, incompressible: true, collapsible: false }); - } + // if (item.element.input.visible) { + // children.push({ element: item.element.input, incompressible: true, collapsible: false }); + // } - if (hasSomeChanges || (this.items.size === 1 && (!this.showActionButton || !item.element.provider.actionButton))) { - children.push(...item.groupItems.map(i => this.render(i, treeViewState))); - } + // if (hasSomeChanges || (this.items.size === 1 && (!this.showActionButton || !item.element.provider.actionButton))) { + // children.push(...item.groupItems.map(i => this.render(i, treeViewState))); + // } - if (this.showActionButton && item.element.provider.actionButton) { - const button: ICompressedTreeElement = { - element: { - type: 'actionButton', - repository: item.element, - button: item.element.provider.actionButton, - }, - incompressible: true, - collapsible: false - }; - children.push(button); - } + // if (this.showActionButton && item.element.provider.actionButton) { + // const button: ICompressedTreeElement = { + // element: { + // type: 'actionButton', + // repository: item.element, + // button: item.element.provider.actionButton, + // }, + // incompressible: true, + // collapsible: false + // }; + // children.push(button); + // } - const collapsed = treeViewState ? treeViewState.collapsed.indexOf(getSCMResourceId(item.element)) > -1 : false; + // const collapsed = treeViewState ? treeViewState.collapsed.indexOf(getSCMResourceId(item.element)) > -1 : false; - return { element: item.element, children, incompressible: true, collapsed, collapsible: true }; - } else { - const children = this.mode === ViewModelMode.List - ? Iterable.map(item.resources, element => ({ element, incompressible: true })) - : Iterable.map(item.tree.root.children, node => asTreeElement(node, true, treeViewState)); + // return { element: item.element, children, incompressible: true, collapsed, collapsible: true }; + // } else { + // const children = this.mode === ViewModelMode.List + // ? Iterable.map(item.resources, element => ({ element, incompressible: true })) + // : Iterable.map(item.tree.root.children, node => asTreeElement(node, true, treeViewState)); - const collapsed = treeViewState ? treeViewState.collapsed.indexOf(getSCMResourceId(item.element)) > -1 : false; + // const collapsed = treeViewState ? treeViewState.collapsed.indexOf(getSCMResourceId(item.element)) > -1 : false; - return { element: item.element, children, incompressible: true, collapsed, collapsible: true }; - } - } + // return { element: item.element, children, incompressible: true, collapsed, collapsible: true }; + // } + // } private updateViewState(): void { const collapsed: string[] = []; @@ -1539,8 +1568,8 @@ class ViewModel { return; } - this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r))); - this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); + // this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r))); + // this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); } collapseAllRepositories(): void { @@ -2302,7 +2331,7 @@ export class SCMViewPane extends ViewPane { private layoutCache: ISCMLayout; private listContainer!: HTMLElement; - private tree!: WorkbenchCompressibleObjectTree; + private tree!: WorkbenchCompressibleAsyncDataTree; private _viewModel!: ViewModel; get viewModel(): ViewModel { return this._viewModel; } private listLabels!: ResourceLabels; @@ -2361,7 +2390,7 @@ export class SCMViewPane extends ViewPane { this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'), this.disposables)(updateProviderCountVisibility)); updateProviderCountVisibility(); - this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, overflowWidgetsDomNode, (input, height) => this.tree.updateElementHeight(input, height), getActionViewItemProvider(this.instantiationService)); + this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, overflowWidgetsDomNode, (input, height) => { console.log('update element height'); /*this.tree.updateElementHeight(input, height)*/ }, getActionViewItemProvider(this.instantiationService)); const delegate = new ListDelegate(this.inputRenderer); this.actionButtonRenderer = this.instantiationService.createInstance(ActionButtonRenderer); @@ -2388,11 +2417,13 @@ export class SCMViewPane extends ViewPane { const dnd = new SCMTreeDragAndDrop(this.instantiationService); this.tree = this.instantiationService.createInstance( - WorkbenchCompressibleObjectTree, + WorkbenchCompressibleAsyncDataTree, 'SCM Tree Repo', this.listContainer, delegate, + new SCMTreeCompressionDelegate(), renderers, + this.instantiationService.createInstance(SCMTreeDataSource, () => this._viewModel), { transformOptimization: false, identityProvider, @@ -2405,8 +2436,9 @@ export class SCMViewPane extends ViewPane { overrideStyles: { listBackground: this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND }, + collapseByDefault: () => false, accessibilityProvider: this.instantiationService.createInstance(SCMAccessibilityProvider) - }) as WorkbenchCompressibleObjectTree; + }) as WorkbenchCompressibleAsyncDataTree; this._register(this.tree.onDidOpen(this.open, this)); @@ -2614,6 +2646,82 @@ export class SCMViewPane extends ViewPane { } } +class SCMTreeDataSource implements IAsyncDataSource { + + constructor( + private readonly viewModel: () => ViewModel, + @ISCMViewService private readonly scmViewService: ISCMViewService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService) { } + + hasChildren(element: TreeElement): boolean { + if (isSCMRepository(element)) { + return true; + } else if (isSCMInput(element)) { + return false; + } else if (isSCMActionButton(element)) { + return false; + } else if (isSCMResourceGroup(element)) { + return element.elements.length > 0; + } else if (isSCMResource(element)) { + return false; + } else if (ResourceTree.isResourceNode(element)) { + return element.childrenCount > 0; + } else { + throw new Error('hasChildren not implemented.'); + } + } + + getChildren(element: TreeElement): Iterable | Promise> { + const children: TreeElement[] = []; + + if (isSCMRepository(element)) { + const provider = element.provider; + const showActionButton = this.viewModel().showActionButton; + const repositoryCount = this.scmViewService.visibleRepositories.length; + + // SCM Input + if (element.input.visible) { + children.push(element.input); + } + + // Action Button + if (showActionButton && provider.actionButton) { + children.push({ + type: 'actionButton', + repository: element, + button: provider.actionButton + } as ISCMActionButton); + } + + // ResourceGroups + const hasSomeChanges = provider.groups.elements.some(item => item.elements.length > 0); + if (hasSomeChanges || (repositoryCount === 1 && (!showActionButton || !provider.actionButton))) { + children.push(...provider.groups.elements); + } + } else if (isSCMResourceGroup(element)) { + if (this.viewModel().mode === ViewModelMode.List) { + // Resources (List) + children.push(...element.elements); + } else if (this.viewModel().mode === ViewModelMode.Tree) { + // Resources (Tree) + const rootUri = element.provider.rootUri ?? URI.file('/'); + const tree = new ResourceTree(element, rootUri, this.uriIdentityService.extUri); + for (const resource of element.elements) { + tree.add(resource.sourceUri, resource); + } + + children.push(...tree.root.children); + } + } else if (ResourceTree.isResourceNode(element)) { + // Resources (Tree) + children.push(...element.children); + } + + return children; + } + +} + export class SCMActionButton implements IDisposable { private button: Button | ButtonWithDescription | ButtonWithDropdown | undefined; private readonly disposables = new MutableDisposable(); From adc17815d0356c7933e23ad004793093e089b110 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 19 Oct 2023 21:49:51 +0200 Subject: [PATCH 02/26] Saving my work. More cleanup to do. --- src/vs/base/browser/ui/tree/asyncDataTree.ts | 9 + src/vs/base/common/lifecycle.ts | 4 + src/vs/workbench/api/browser/mainThreadSCM.ts | 61 ++- .../browser/editSessions.contribution.ts | 4 +- .../workbench/contrib/scm/browser/activity.ts | 6 +- src/vs/workbench/contrib/scm/browser/menus.ts | 21 +- .../contrib/scm/browser/scmViewPane.ts | 469 +++++++----------- src/vs/workbench/contrib/scm/browser/util.ts | 8 +- src/vs/workbench/contrib/scm/common/scm.ts | 18 +- 9 files changed, 257 insertions(+), 343 deletions(-) diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 5ba79157f2d0a..be95156a79b0e 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -559,6 +559,10 @@ export class AsyncDataTree implements IDisposable this.tree.resort(this.getDataNode(element), recursive); } + hasElement(element: TInput | T): boolean { + return this.tree.hasElement(this.getDataNode(element)); + } + hasNode(element: TInput | T): boolean { return element === this.root.element || this.nodes.has(element as T); } @@ -575,6 +579,11 @@ export class AsyncDataTree implements IDisposable this.tree.rerender(node); } + updateElementHeight(element: T, height: number | undefined): void { + const node = this.getDataNode(element); + this.tree.updateElementHeight(node, height); + } + updateWidth(element: T): void { const node = this.getDataNode(element); this.tree.updateWidth(node); diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 4b6215299461a..fedbb85a6cf33 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -748,6 +748,10 @@ export class DisposableMap implements ID this._store.delete(key); } + keys(): IterableIterator { + return this._store.keys(); + } + [Symbol.iterator](): IterableIterator<[K, V]> { return this._store[Symbol.iterator](); } diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 5ddf13e573e24..4675fd247c6b9 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -10,13 +10,14 @@ import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGr import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemDto, SCMActionButtonDto, SCMHistoryItemGroupDto } from '../common/extHost.protocol'; import { Command } from 'vs/editor/common/languages'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ISplice, Sequence } from 'vs/base/common/sequence'; import { CancellationToken } from 'vs/base/common/cancellation'; import { MarshalledId } from 'vs/base/common/marshallingIds'; import { ThemeIcon } from 'vs/base/common/themables'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IQuickDiffService, QuickDiffProvider } from 'vs/workbench/contrib/scm/common/quickDiff'; import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryOptions, ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history'; +import { ResourceTree } from 'vs/base/common/resourceTree'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; function getSCMHistoryItemIcon(historyItem: SCMHistoryItemDto): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (!historyItem.icon) { @@ -33,23 +34,37 @@ function getSCMHistoryItemIcon(historyItem: SCMHistoryItemDto): URI | { light: U class MainThreadSCMResourceGroup implements ISCMResourceGroup { - readonly elements: ISCMResource[] = []; + readonly resources: ISCMResource[] = []; - private readonly _onDidSplice = new Emitter>(); - readonly onDidSplice = this._onDidSplice.event; + private _resourceTree: ResourceTree | undefined; + get resourceTree(): ResourceTree { + if (!this._resourceTree) { + const rootUri = this.provider.rootUri ?? URI.file('/'); + this._resourceTree = new ResourceTree(this, rootUri, this._uriIdentService.extUri); + for (const resource of this.resources) { + this._resourceTree.add(resource.sourceUri, resource); + } + } - get hideWhenEmpty(): boolean { return !!this.features.hideWhenEmpty; } + return this._resourceTree; + } private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; + private readonly _onDidChangeResources = new Emitter(); + readonly onDidChangeResources = this._onDidChangeResources.event; + + get hideWhenEmpty(): boolean { return !!this.features.hideWhenEmpty; } + constructor( private readonly sourceControlHandle: number, private readonly handle: number, public provider: ISCMProvider, public features: SCMGroupFeatures, public label: string, - public id: string + public id: string, + private readonly _uriIdentService: IUriIdentityService ) { } toJSON(): any { @@ -61,8 +76,10 @@ class MainThreadSCMResourceGroup implements ISCMResourceGroup { } splice(start: number, deleteCount: number, toInsert: ISCMResource[]) { - this.elements.splice(start, deleteCount, ...toInsert); - this._onDidSplice.fire({ start, deleteCount, toInsert }); + this.resources.splice(start, deleteCount, ...toInsert); + this._resourceTree = undefined; + + this._onDidChangeResources.fire(); } $updateGroup(features: SCMGroupFeatures): void { @@ -159,7 +176,13 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private _id = `scm${MainThreadSCMProvider.ID_HANDLE++}`; get id(): string { return this._id; } - readonly groups = new Sequence(); + readonly groups: MainThreadSCMResourceGroup[] = []; + private readonly _onDidChangeResourceGroups = new Emitter(); + readonly onDidChangeResourceGroups = this._onDidChangeResourceGroups.event; + + private readonly _onDidChangeResources = new Emitter(); + readonly onDidChangeResources = this._onDidChangeResources.event; + private readonly _groupsByHandle: { [handle: number]: MainThreadSCMResourceGroup } = Object.create(null); // get groups(): ISequence { @@ -172,8 +195,6 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { // // .filter(g => g.resources.elements.length > 0 || !g.features.hideWhenEmpty); // } - private readonly _onDidChangeResources = new Emitter(); - readonly onDidChangeResources: Event = this._onDidChangeResources.event; private features: SCMProviderFeatures = {}; @@ -214,7 +235,8 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private readonly _label: string, private readonly _rootUri: URI | undefined, private readonly _inputBoxDocumentUri: URI, - private readonly _quickDiffService: IQuickDiffService + private readonly _quickDiffService: IQuickDiffService, + private readonly _uriIdentService: IUriIdentityService ) { } $updateSourceControl(features: SCMProviderFeatures): void { @@ -258,14 +280,16 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { this, features, label, - id + id, + this._uriIdentService ); this._groupsByHandle[handle] = group; return group; }); - this.groups.splice(this.groups.elements.length, 0, groups); + this.groups.splice(this.groups.length, 0, ...groups); + this._onDidChangeResourceGroups.fire(); } $updateGroup(handle: number, features: SCMGroupFeatures): void { @@ -344,8 +368,8 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { } delete this._groupsByHandle[handle]; - this.groups.splice(this.groups.elements.indexOf(group), 1); - this._onDidChangeResources.fire(); + this.groups.splice(this.groups.indexOf(group), 1); + this._onDidChangeResourceGroups.fire(); } async getOriginalResource(uri: URI): Promise { @@ -397,7 +421,8 @@ export class MainThreadSCM implements MainThreadSCMShape { extHostContext: IExtHostContext, @ISCMService private readonly scmService: ISCMService, @ISCMViewService private readonly scmViewService: ISCMViewService, - @IQuickDiffService private readonly quickDiffService: IQuickDiffService + @IQuickDiffService private readonly quickDiffService: IQuickDiffService, + @IUriIdentityService private readonly _uriIdentService: IUriIdentityService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSCM); } @@ -413,7 +438,7 @@ export class MainThreadSCM implements MainThreadSCMShape { } $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void { - const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, URI.revive(inputBoxDocumentUri), this.quickDiffService); + const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, URI.revive(inputBoxDocumentUri), this.quickDiffService, this._uriIdentService); const repository = this.scmService.registerSCMProvider(provider); this._repositories.set(handle, repository); diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index bac592ba3a62c..7ba7a0095403f 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -788,8 +788,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } private getChangedResources(repository: ISCMRepository) { - return repository.provider.groups.elements.reduce((resources, resourceGroups) => { - resourceGroups.elements.forEach((resource) => resources.add(resource.sourceUri)); + return repository.provider.groups.reduce((resources, resourceGroups) => { + resourceGroups.resources.forEach((resource) => resources.add(resource.sourceUri)); return resources; }, new Set()); // A URI might appear in more than one resource group } diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index b4edaa7f64e39..68a280a56bca3 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -23,7 +23,7 @@ function getCount(repository: ISCMRepository): number { if (typeof repository.provider.count === 'number') { return repository.provider.count; } else { - return repository.provider.groups.elements.reduce((r, g) => r + g.elements.length, 0); + return repository.provider.groups.reduce((r, g) => r + g.resources.length, 0); } } @@ -264,8 +264,8 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu this.activeResourceRepositoryContextKey.set(activeResourceRepository?.id); - for (const resourceGroup of activeResourceRepository?.provider.groups.elements ?? []) { - if (resourceGroup.elements + for (const resourceGroup of activeResourceRepository?.provider.groups ?? []) { + if (resourceGroup.resources .some(scmResource => this.uriIdentityService.extUri.isEqual(activeResource, scmResource.sourceUri))) { this.activeResourceHasChangesContextKey.set(true); diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index d1bde83d780b2..86887f8d64581 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -12,7 +12,6 @@ import { IAction } from 'vs/base/common/actions'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ISCMResource, ISCMResourceGroup, ISCMProvider, ISCMRepository, ISCMService, ISCMMenus, ISCMRepositoryMenus } from 'vs/workbench/contrib/scm/common/scm'; import { equals } from 'vs/base/common/arrays'; -import { ISplice } from 'vs/base/common/sequence'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { localize } from 'vs/nls'; @@ -148,7 +147,6 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { private contextKeyService: IContextKeyService; readonly titleMenu: SCMTitleMenu; - private readonly resourceGroups: ISCMResourceGroup[] = []; private readonly resourceGroupMenusItems = new Map(); private _repositoryMenu: IMenu | undefined; @@ -174,7 +172,7 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { private readonly disposables = new DisposableStore(); constructor( - provider: ISCMProvider, + private readonly provider: ISCMProvider, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, @IMenuService private readonly menuService: IMenuService @@ -189,8 +187,8 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { instantiationService = instantiationService.createChild(serviceCollection); this.titleMenu = instantiationService.createInstance(SCMTitleMenu); - provider.groups.onDidSplice(this.onDidSpliceGroups, this, this.disposables); - this.onDidSpliceGroups({ start: 0, deleteCount: 0, toInsert: provider.groups.elements }); + provider.onDidChangeResourceGroups(this.onDidChangeResourceGroups, this, this.disposables); + this.onDidChangeResourceGroups(); } getResourceGroupMenu(group: ISCMResourceGroup): IMenu { @@ -220,13 +218,12 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { return result; } - private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice): void { - const deleted = this.resourceGroups.splice(start, deleteCount, ...toInsert); - - for (const group of deleted) { - const item = this.resourceGroupMenusItems.get(group); - item?.dispose(); - this.resourceGroupMenusItems.delete(group); + private onDidChangeResourceGroups(): void { + for (const resourceGroup of this.resourceGroupMenusItems.keys()) { + if (!this.provider.groups.includes(resourceGroup)) { + this.resourceGroupMenusItems.get(resourceGroup)?.dispose(); + this.resourceGroupMenusItems.delete(resourceGroup); + } } } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 43a76e400708a..d2bec46002c6c 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/scm'; import { Event, Emitter } from 'vs/base/common/event'; import { basename, dirname } from 'vs/base/common/resources'; -import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, IReference } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, IReference, DisposableMap } from 'vs/base/common/lifecycle'; import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; @@ -23,13 +23,12 @@ import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, import { IAction, ActionRunner, Action, Separator } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton } from './util'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService } from './util'; import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, ThrottledDelayer } from 'vs/base/common/async'; import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { ResourceTree, IResourceNode } from 'vs/base/common/resourceTree'; -import { ISplice } from 'vs/base/common/sequence'; import { ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/tree/objectTree'; import { Iterable } from 'vs/base/common/iterator'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; @@ -73,7 +72,6 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { LabelFuzzyScore } from 'vs/base/browser/ui/tree/abstractTree'; import { Selection } from 'vs/editor/common/core/selection'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; @@ -96,7 +94,8 @@ import { fillEditorsDragData } from 'vs/workbench/browser/dnd'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { CodeDataTransfers } from 'vs/platform/dnd/browser/dnd'; import { FormatOnType } from 'vs/editor/contrib/format/browser/formatActions'; -import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { IAsyncDataTreeViewState, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | IResourceNode | ISCMResource; @@ -405,7 +404,7 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer { if (ResourceTree.isResourceNode(element)) { return true; } else if (isSCMResourceGroup(element)) { - return element.elements.length > 0 || !element.hideWhenEmpty; + return element.resources.length > 0 || !element.hideWhenEmpty; } else { return true; } @@ -952,40 +951,6 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider; - dispose(): void; -} - -interface IRepositoryItem { - readonly element: ISCMRepository; - readonly groupItems: IGroupItem[]; - dispose(): void; -} - -interface ITreeViewState { - readonly collapsed: string[]; -} - -function isRepositoryItem(item: IRepositoryItem | IGroupItem): item is IRepositoryItem { - return Array.isArray((item as IRepositoryItem).groupItems); -} - -// function asTreeElement(node: IResourceNode, forceIncompressible: boolean, viewState?: ITreeViewState): ICompressedTreeElement { -// const element = (node.childrenCount === 0 && node.element) ? node.element : node; -// const collapsed = viewState ? viewState.collapsed.indexOf(getSCMResourceId(element)) > -1 : false; - -// return { -// element, -// children: Iterable.map(node.children, node => asTreeElement(node, false, viewState)), -// incompressible: !!node.element || forceIncompressible, -// collapsed, -// collapsible: node.childrenCount > 0 -// }; -// } - const enum ViewModelMode { List = 'list', Tree = 'tree' @@ -1155,22 +1120,10 @@ class ViewModel { this._mode = mode; - // for (const [, item] of this.items) { - // for (const groupItem of item.groupItems) { - // groupItem.tree.clear(); - - // if (mode === ViewModelMode.Tree) { - // for (const resource of groupItem.resources) { - // groupItem.tree.add(resource.sourceUri, resource); - // } - // } - // } - // } - // Update sort key based on view mode this.sortKey = this.getViewModelSortKey(); - this.refresh(); + this.tree.updateChildren(); this._onDidChangeMode.fire(mode); this.modeContextKey.set(mode); @@ -1185,7 +1138,7 @@ class ViewModel { this._sortKey = sortKey; - this.refresh(); + this.tree.updateChildren(); this._onDidChangeSortKey.fire(sortKey); this.sortKeyContextKey.set(sortKey); @@ -1194,23 +1147,15 @@ class ViewModel { } } - private _treeViewStateIsStale = false; - get treeViewState(): ITreeViewState | undefined { - if (this.visible && this._treeViewStateIsStale) { - this.updateViewState(); - this._treeViewStateIsStale = false; - } - - return this._treeViewState; - } - private _showActionButton = false; get showActionButton(): boolean { return this._showActionButton; } - private items = new Map(); + private _alwaysShowRepositories = false; + get alwaysShowRepositories(): boolean { return this._alwaysShowRepositories; } + + private items = new DisposableMap(); private readonly visibilityDisposables = new DisposableStore(); private scrollTop: number | undefined; - private alwaysShowRepositories = false; private firstVisible = true; private readonly disposables = new DisposableStore(); @@ -1224,10 +1169,9 @@ class ViewModel { private _mode: ViewModelMode; private _sortKey: ViewModelSortKey; - private _treeViewState: ITreeViewState | undefined; constructor( - private tree: WorkbenchCompressibleAsyncDataTree, + private tree: WorkbenchCompressibleAsyncDataTree, private inputRenderer: InputRenderer, @IInstantiationService protected instantiationService: IInstantiationService, @IEditorService protected editorService: IEditorService, @@ -1241,14 +1185,6 @@ class ViewModel { this._mode = this.getViewModelMode(); this._sortKey = this.getViewModelSortKey(); - // TreeView state - const storageViewState = this.storageService.get(`scm.viewState`, StorageScope.WORKSPACE); - if (storageViewState) { - try { - this._treeViewState = JSON.parse(storageViewState); - } catch {/* noop */ } - } - this.modeContextKey = ContextKeys.ViewModelMode.bindTo(contextKeyService); this.modeContextKey.set(this._mode); this.sortKeyContextKey = ContextKeys.ViewModelSortKey.bindTo(contextKeyService); @@ -1265,13 +1201,7 @@ class ViewModel { Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element), this.disposables) (this.updateRepositoryCollapseAllContextKeys, this, this.disposables); - this.disposables.add(this.tree.onDidChangeCollapseState(() => this._treeViewStateIsStale = true)); - this.disposables.add(this.storageService.onWillSaveState(e => { - if (e.reason === WillSaveStateReason.SHUTDOWN) { - this.storageService.store(`scm.viewState`, JSON.stringify(this.treeViewState), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - this.mode = this.getViewModelMode(); this.sortKey = this.getViewModelSortKey(); })); @@ -1290,93 +1220,55 @@ class ViewModel { private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { if (!e || e.affectsConfiguration('scm.alwaysShowRepositories') || e.affectsConfiguration('scm.showActionButton')) { - this.alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories'); + this._alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories'); this._showActionButton = this.configurationService.getValue('scm.showActionButton'); - this.refresh(); + + this.tree.updateChildren(); + this.updateRepositoryContextKeys(); } } private _onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void { for (const repository of added) { - const disposable = combinedDisposable( - repository.provider.groups.onDidSplice(splice => this._onDidSpliceGroups(item, splice)), - repository.input.onDidChangeVisibility(() => this.refresh(item.element)), - repository.provider.onDidChange(() => { - if (this.showActionButton) { - this.refresh(item.element); - } - }) - ); - const groupItems = repository.provider.groups.elements.map(group => this.createGroupItem(group)); - const item: IRepositoryItem = { - element: repository, groupItems, dispose() { - dispose(this.groupItems); - disposable.dispose(); - } - }; + const repositoryDisposables = new DisposableStore(); - this.items.set(repository, item); - } + repositoryDisposables.add(repository.provider.onDidChange(() => this.tree.updateChildren(repository, true, true))); + repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(() => this.tree.updateChildren(repository, true))); + repositoryDisposables.add(repository.input.onDidChangeVisibility(() => this.tree.updateChildren(repository))); - for (const repository of removed) { - const item = this.items.get(repository)!; - item.dispose(); - this.items.delete(repository); - } - - this.refresh(); - } + const resourceGroupDisposables = repositoryDisposables.add(new DisposableMap()); - private _onDidSpliceGroups(item: IRepositoryItem, { start, deleteCount, toInsert }: ISplice): void { - const itemsToInsert: IGroupItem[] = toInsert.map(group => this.createGroupItem(group)); - const itemsToDispose = item.groupItems.splice(start, deleteCount, ...itemsToInsert); - - for (const item of itemsToDispose) { - item.dispose(); - } + const onDidChangeResourceGroups = () => { + for (const [resourceGroup] of resourceGroupDisposables) { + if (!repository.provider.groups.includes(resourceGroup)) { + resourceGroupDisposables.deleteAndDispose(resourceGroup); + } + } - this.refresh(); - } + for (const resourceGroup of repository.provider.groups) { + if (!resourceGroupDisposables.has(resourceGroup)) { + const disposableStore = new DisposableStore(); - private createGroupItem(group: ISCMResourceGroup): IGroupItem { - const tree = new ResourceTree(group, group.provider.rootUri || URI.file('/'), this.uriIdentityService.extUri); - const resources: ISCMResource[] = [...group.elements]; - const disposable = combinedDisposable( - group.onDidChange(() => this.tree.refilter()), - group.onDidSplice(splice => this._onDidSpliceGroup(item, splice)) - ); + disposableStore.add(resourceGroup.onDidChange(() => this.tree.updateChildren(repository))); + disposableStore.add(resourceGroup.onDidChangeResources(() => this.tree.updateChildren(resourceGroup))); + resourceGroupDisposables.set(resourceGroup, disposableStore); + } + } + }; - const item: IGroupItem = { element: group, resources, tree, dispose() { disposable.dispose(); } }; + repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(onDidChangeResourceGroups)); + onDidChangeResourceGroups(); - if (this._mode === ViewModelMode.Tree) { - for (const resource of resources) { - item.tree.add(resource.sourceUri, resource); - } + this.items.set(repository, repositoryDisposables); } - return item; - } - - private _onDidSpliceGroup(item: IGroupItem, { start, deleteCount, toInsert }: ISplice): void { - const before = item.resources.length; - const deleted = item.resources.splice(start, deleteCount, ...toInsert); - const after = item.resources.length; - - if (this._mode === ViewModelMode.Tree) { - for (const resource of deleted) { - item.tree.delete(resource.sourceUri); - } - - for (const resource of toInsert) { - item.tree.add(resource.sourceUri, resource); - } + for (const repository of removed) { + this.items.deleteAndDispose(repository); } - if (before !== after && (before === 0 || after === 0)) { - this.refresh(); - } else { - this.refresh(item.element); - } + this.tree.updateChildren(); + this.updateRepositoryContextKeys(); + this.updateRepositoryCollapseAllContextKeys(); } setVisible(visible: boolean): void { @@ -1392,118 +1284,60 @@ class ViewModel { this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); this.onDidActiveEditorChange(); } else { - this.updateViewState(); - this.visibilityDisposables.clear(); this._onDidChangeVisibleRepositories({ added: Iterable.empty(), removed: [...this.items.keys()] }); this.scrollTop = this.tree.scrollTop; } this.visible = visible; + this.updateRepositoryContextKeys(); this.updateRepositoryCollapseAllContextKeys(); } - private async refresh(item?: ISCMRepository | ISCMResourceGroup): Promise { - if (!this.alwaysShowRepositories && this.items.size === 1) { - const provider = Iterable.first(this.items.values())!.element.provider; - this.scmProviderContextKey.set(provider.contextValue); - this.scmProviderRootUriContextKey.set(provider.rootUri?.toString()); - this.scmProviderHasRootUriContextKey.set(!!provider.rootUri); - } else { - this.scmProviderContextKey.set(undefined); - this.scmProviderRootUriContextKey.set(undefined); - this.scmProviderHasRootUriContextKey.set(false); - } - - const focusedInput = this.inputRenderer.getFocusedInput(); - - if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isSCMRepository(item)))) { - // Single repository and not always show repositories - await this.tree.setInput(Iterable.first(this.items.keys())!); - } else if (item) { - // Particular repository or resource group - await this.tree.updateChildren(item); - } else { - // Expand repository nodes - // const expanded = Array.from(this.items.keys()) - // .map(repository => `repo:${repository.provider.id}`); - - // await this.tree.setInput([...this.items.keys()], { expanded }); - } - - // if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isRepositoryItem(item)))) { - // const item = Iterable.first(this.items.values())!; - // this.tree.setChildren(null, this.render(item, this.treeViewState).children); - // } else if (item) { - // this.tree.setChildren(item.element, this.render(item, this.treeViewState).children); - // } else { - // const items = coalesce(this.scmViewService.visibleRepositories.map(r => this.items.get(r))); - // this.tree.setChildren(null, items.map(item => this.render(item, this.treeViewState))); - // } - - if (focusedInput) { - this.inputRenderer.getRenderedInputWidget(focusedInput)?.focus(); - } - - this.updateRepositoryCollapseAllContextKeys(); - } - - // private render(item: IRepositoryItem | IGroupItem, treeViewState?: ITreeViewState): ICompressedTreeElement { - // if (isRepositoryItem(item)) { - // const children: ICompressedTreeElement[] = []; - // const hasSomeChanges = item.groupItems.some(item => item.element.elements.length > 0); - - // if (item.element.input.visible) { - // children.push({ element: item.element.input, incompressible: true, collapsible: false }); - // } - - // if (hasSomeChanges || (this.items.size === 1 && (!this.showActionButton || !item.element.provider.actionButton))) { - // children.push(...item.groupItems.map(i => this.render(i, treeViewState))); - // } - - // if (this.showActionButton && item.element.provider.actionButton) { - // const button: ICompressedTreeElement = { - // element: { - // type: 'actionButton', - // repository: item.element, - // button: item.element.provider.actionButton, - // }, - // incompressible: true, - // collapsible: false - // }; - // children.push(button); - // } + // private async refresh(item?: ISCMRepository | ISCMResourceGroup): Promise { + // if (!this.alwaysShowRepositories && this.items.size === 1) { + // const provider = Iterable.first(this.items.keys())!.provider; + // this.scmProviderContextKey.set(provider.contextValue); + // this.scmProviderRootUriContextKey.set(provider.rootUri?.toString()); + // this.scmProviderHasRootUriContextKey.set(!!provider.rootUri); + // } else { + // this.scmProviderContextKey.set(undefined); + // this.scmProviderRootUriContextKey.set(undefined); + // this.scmProviderHasRootUriContextKey.set(false); + // } - // const collapsed = treeViewState ? treeViewState.collapsed.indexOf(getSCMResourceId(item.element)) > -1 : false; + // const focusedInput = this.inputRenderer.getFocusedInput(); - // return { element: item.element, children, incompressible: true, collapsed, collapsible: true }; + // if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isSCMRepository(item)))) { + // // Single repository and not always show repositories + // await this.tree.setInput(Iterable.first(this.items.keys())!); + // } else if (item) { + // // Particular repository or resource group + // await this.tree.updateChildren(item); // } else { - // const children = this.mode === ViewModelMode.List - // ? Iterable.map(item.resources, element => ({ element, incompressible: true })) - // : Iterable.map(item.tree.root.children, node => asTreeElement(node, true, treeViewState)); - - // const collapsed = treeViewState ? treeViewState.collapsed.indexOf(getSCMResourceId(item.element)) > -1 : false; + // // Expand repository nodes + // // const expanded = Array.from(this.items.keys()) + // // .map(repository => `repo:${repository.provider.id}`); - // return { element: item.element, children, incompressible: true, collapsed, collapsible: true }; + // // await this.tree.setInput([...this.items.keys()], { expanded }); // } - // } - - private updateViewState(): void { - const collapsed: string[] = []; - const visit = (node: ITreeNode) => { - if (node.element && node.collapsible && node.collapsed) { - collapsed.push(getSCMResourceId(node.element)); - } - - for (const child of node.children) { - visit(child); - } - }; - visit(this.tree.getNode()); + // // if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isRepositoryItem(item)))) { + // // const item = Iterable.first(this.items.values())!; + // // this.tree.setChildren(null, this.render(item, this.treeViewState).children); + // // } else if (item) { + // // this.tree.setChildren(item.element, this.render(item, this.treeViewState).children); + // // } else { + // // const items = coalesce(this.scmViewService.visibleRepositories.map(r => this.items.get(r))); + // // this.tree.setChildren(null, items.map(item => this.render(item, this.treeViewState))); + // // } + + // if (focusedInput) { + // this.inputRenderer.getRenderedInputWidget(focusedInput)?.focus(); + // } - this._treeViewState = { collapsed }; - } + // this.updateRepositoryCollapseAllContextKeys(); + // } private onDidActiveEditorChange(): void { if (!this.configurationService.getValue('scm.autoReveal')) { @@ -1530,10 +1364,10 @@ class ViewModel { } // go backwards from last group - for (let j = item.groupItems.length - 1; j >= 0; j--) { - const groupItem = item.groupItems[j]; + for (let j = repository.provider.groups.length - 1; j >= 0; j--) { + const groupItem = repository.provider.groups[j]; const resource = this.mode === ViewModelMode.Tree - ? groupItem.tree.getNode(uri)?.element + ? groupItem.resourceTree.getNode(uri)?.element : groupItem.resources.find(r => this.uriIdentityService.extUri.isEqual(r.sourceUri, uri)); if (resource) { @@ -1561,6 +1395,19 @@ class ViewModel { this.tree.domFocus(); } + private updateRepositoryContextKeys(): void { + if (!this.alwaysShowRepositories && this.items.size === 1) { + const provider = Iterable.first(this.items.keys())!.provider; + this.scmProviderContextKey.set(provider.contextValue); + this.scmProviderRootUriContextKey.set(provider.rootUri?.toString()); + this.scmProviderHasRootUriContextKey.set(!!provider.rootUri); + } else { + this.scmProviderContextKey.set(undefined); + this.scmProviderRootUriContextKey.set(undefined); + this.scmProviderHasRootUriContextKey.set(false); + } + } + private updateRepositoryCollapseAllContextKeys(): void { if (!this.visible || this.scmViewService.visibleRepositories.length === 1) { this.isAnyRepositoryCollapsibleContextKey.set(false); @@ -1568,8 +1415,8 @@ class ViewModel { return; } - // this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r))); - // this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); + this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r))); + this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); } collapseAllRepositories(): void { @@ -1630,8 +1477,7 @@ class ViewModel { dispose(): void { this.visibilityDisposables.dispose(); this.disposables.dispose(); - dispose(this.items.values()); - this.items.clear(); + this.items.dispose(); } } @@ -2331,7 +2177,7 @@ export class SCMViewPane extends ViewPane { private layoutCache: ISCMLayout; private listContainer!: HTMLElement; - private tree!: WorkbenchCompressibleAsyncDataTree; + private tree!: WorkbenchCompressibleAsyncDataTree; private _viewModel!: ViewModel; get viewModel(): ViewModel { return this._viewModel; } private listLabels!: ResourceLabels; @@ -2354,6 +2200,7 @@ export class SCMViewPane extends ViewPane { @IContextKeyService contextKeyService: IContextKeyService, @IMenuService private menuService: IMenuService, @IOpenerService openerService: IOpenerService, + @IStorageService private storageService: IStorageService, @ITelemetryService telemetryService: ITelemetryService, ) { super({ ...options, titleMenuId: MenuId.SCMTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); @@ -2366,8 +2213,13 @@ export class SCMViewPane extends ViewPane { }; this._register(this.instantiationService.createInstance(ScmInputContentProvider)); - this._register(Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire())); + + this._register(this.storageService.onWillSaveState(e => { + if (e.reason === WillSaveStateReason.SHUTDOWN) { + this.storeTreeViewState(); + } + })); } protected override renderBody(container: HTMLElement): void { @@ -2390,7 +2242,7 @@ export class SCMViewPane extends ViewPane { this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'), this.disposables)(updateProviderCountVisibility)); updateProviderCountVisibility(); - this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, overflowWidgetsDomNode, (input, height) => { console.log('update element height'); /*this.tree.updateElementHeight(input, height)*/ }, getActionViewItemProvider(this.instantiationService)); + this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, overflowWidgetsDomNode, (input, height) => { this.tree.updateElementHeight(input, height); }, getActionViewItemProvider(this.instantiationService)); const delegate = new ListDelegate(this.inputRenderer); this.actionButtonRenderer = this.instantiationService.createInstance(ActionButtonRenderer); @@ -2436,9 +2288,9 @@ export class SCMViewPane extends ViewPane { overrideStyles: { listBackground: this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND }, - collapseByDefault: () => false, + collapseByDefault: (e) => false, accessibilityProvider: this.instantiationService.createInstance(SCMAccessibilityProvider) - }) as WorkbenchCompressibleAsyncDataTree; + }) as WorkbenchCompressibleAsyncDataTree; this._register(this.tree.onDidOpen(this.open, this)); @@ -2447,14 +2299,14 @@ export class SCMViewPane extends ViewPane { this._register(this.tree); append(this.listContainer, overflowWidgetsDomNode); + this.listContainer.classList.add('file-icon-themable-tree'); + this.listContainer.classList.add('show-file-icons'); this._register(this.instantiationService.createInstance(RepositoryVisibilityActionController)); - this._viewModel = this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer); - this._register(this._viewModel); - - this.listContainer.classList.add('file-icon-themable-tree'); - this.listContainer.classList.add('show-file-icons'); + // TODO - @lszomoru + this._viewModel = this._register(this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer)); + this.tree.setInput(this.scmViewService, this.loadTreeViewState()); this.updateIndentStyles(this.themeService.getFileIconTheme()); this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); @@ -2632,6 +2484,24 @@ export class SCMViewPane extends ViewPane { .filter(r => !!r && !isSCMResourceGroup(r))! as any; } + private loadTreeViewState(): IAsyncDataTreeViewState | undefined { + const storageViewState = this.storageService.get(`scm.viewState`, StorageScope.WORKSPACE); + if (!storageViewState) { + return undefined; + } + + try { + const treeViewState = JSON.parse(storageViewState); + return treeViewState; + } catch { + return undefined; + } + } + + private storeTreeViewState() { + this.storageService.store(`scm.viewState`, JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + override shouldShowWelcome(): boolean { return this.scmService.repositoryCount === 0; } @@ -2646,78 +2516,79 @@ export class SCMViewPane extends ViewPane { } } -class SCMTreeDataSource implements IAsyncDataSource { +class SCMTreeDataSource implements IAsyncDataSource { constructor( private readonly viewModel: () => ViewModel, - @ISCMViewService private readonly scmViewService: ISCMViewService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService) { } + @ISCMViewService private readonly scmViewService: ISCMViewService) { } - hasChildren(element: TreeElement): boolean { - if (isSCMRepository(element)) { + hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean { + if (isSCMViewService(inputOrElement)) { + return this.scmViewService.visibleRepositories.length !== 0; + } else if (isSCMRepository(inputOrElement)) { return true; - } else if (isSCMInput(element)) { + } else if (isSCMInput(inputOrElement)) { return false; - } else if (isSCMActionButton(element)) { + } else if (isSCMActionButton(inputOrElement)) { return false; - } else if (isSCMResourceGroup(element)) { - return element.elements.length > 0; - } else if (isSCMResource(element)) { + } else if (isSCMResourceGroup(inputOrElement)) { + return inputOrElement.resources.length > 0; + } else if (isSCMResource(inputOrElement)) { return false; - } else if (ResourceTree.isResourceNode(element)) { - return element.childrenCount > 0; + } else if (ResourceTree.isResourceNode(inputOrElement)) { + return inputOrElement.childrenCount > 0; } else { throw new Error('hasChildren not implemented.'); } } - getChildren(element: TreeElement): Iterable | Promise> { - const children: TreeElement[] = []; + getChildren(inputOrElement: ISCMViewService | TreeElement): Iterable | Promise> { + if (isSCMViewService(inputOrElement)) { + return this.scmViewService.visibleRepositories; + } - if (isSCMRepository(element)) { - const provider = element.provider; + if (isSCMRepository(inputOrElement)) { + const children: TreeElement[] = []; + + const provider = inputOrElement.provider; const showActionButton = this.viewModel().showActionButton; const repositoryCount = this.scmViewService.visibleRepositories.length; // SCM Input - if (element.input.visible) { - children.push(element.input); + if (inputOrElement.input.visible) { + children.push(inputOrElement.input); } // Action Button if (showActionButton && provider.actionButton) { children.push({ type: 'actionButton', - repository: element, + repository: inputOrElement, button: provider.actionButton } as ISCMActionButton); } // ResourceGroups - const hasSomeChanges = provider.groups.elements.some(item => item.elements.length > 0); + const hasSomeChanges = provider.groups.some(item => item.resources.length > 0); if (hasSomeChanges || (repositoryCount === 1 && (!showActionButton || !provider.actionButton))) { - children.push(...provider.groups.elements); + children.push(...provider.groups); } - } else if (isSCMResourceGroup(element)) { + + return children; + } else if (isSCMResourceGroup(inputOrElement)) { if (this.viewModel().mode === ViewModelMode.List) { // Resources (List) - children.push(...element.elements); + return inputOrElement.resources; } else if (this.viewModel().mode === ViewModelMode.Tree) { // Resources (Tree) - const rootUri = element.provider.rootUri ?? URI.file('/'); - const tree = new ResourceTree(element, rootUri, this.uriIdentityService.extUri); - for (const resource of element.elements) { - tree.add(resource.sourceUri, resource); - } - - children.push(...tree.root.children); + return inputOrElement.resourceTree.root.children; } - } else if (ResourceTree.isResourceNode(element)) { + } else if (ResourceTree.isResourceNode(inputOrElement)) { // Resources (Tree) - children.push(...element.children); + return inputOrElement.children; } - return children; + return []; } } diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 34b7170c71e0c..ad83f6369bbea 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { IMenu } from 'vs/platform/actions/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -21,6 +21,10 @@ export function isSCMRepositoryArray(element: any): element is ISCMRepository[] return Array.isArray(element) && element.every(r => isSCMRepository(r)); } +export function isSCMViewService(element: any): element is ISCMViewService { + return Array.isArray((element as ISCMViewService).repositories) && Array.isArray((element as ISCMViewService).visibleRepositories); +} + export function isSCMRepository(element: any): element is ISCMRepository { return !!(element as ISCMRepository).provider && !!(element as ISCMRepository).input; } @@ -34,7 +38,7 @@ export function isSCMActionButton(element: any): element is ISCMActionButton { } export function isSCMResourceGroup(element: any): element is ISCMResourceGroup { - return !!(element as ISCMResourceGroup).provider && !!(element as ISCMResourceGroup).elements; + return !!(element as ISCMResourceGroup).provider && !!(element as ISCMResourceGroup).resources; } export function isSCMResource(element: any): element is ISCMResource { diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index af7852084c7ef..7a85d7e3746ad 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -8,11 +8,11 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Command } from 'vs/editor/common/languages'; -import { ISequence } from 'vs/base/common/sequence'; import { IAction } from 'vs/base/common/actions'; import { IMenu } from 'vs/platform/actions/common/actions'; import { ThemeIcon } from 'vs/base/common/themables'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { ResourceTree } from 'vs/base/common/resourceTree'; import { ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history'; export const VIEWLET_ID = 'workbench.view.scm'; @@ -43,22 +43,26 @@ export interface ISCMResource { open(preserveFocus: boolean): Promise; } -export interface ISCMResourceGroup extends ISequence { +export interface ISCMResourceGroup { + readonly id: string; readonly provider: ISCMProvider; + + readonly resources: readonly ISCMResource[]; + readonly resourceTree: ResourceTree; + readonly onDidChangeResources: Event; + readonly label: string; - readonly id: string; readonly hideWhenEmpty: boolean; readonly onDidChange: Event; } export interface ISCMProvider extends IDisposable { - readonly label: string; readonly id: string; + readonly label: string; readonly contextValue: string; - readonly groups: ISequence; - - // TODO@Joao: remove + readonly groups: readonly ISCMResourceGroup[]; + readonly onDidChangeResourceGroups: Event; readonly onDidChangeResources: Event; readonly rootUri?: URI; From fd9d0972d7a322349ec4d383596b4b226f3cb1bd Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 20 Oct 2023 10:23:56 +0200 Subject: [PATCH 03/26] Saving my changes --- .../contrib/scm/browser/scmViewPane.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index d2bec46002c6c..1ee5618c1ec30 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2299,23 +2299,25 @@ export class SCMViewPane extends ViewPane { this._register(this.tree); append(this.listContainer, overflowWidgetsDomNode); - this.listContainer.classList.add('file-icon-themable-tree'); - this.listContainer.classList.add('show-file-icons'); this._register(this.instantiationService.createInstance(RepositoryVisibilityActionController)); - // TODO - @lszomoru - this._viewModel = this._register(this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer)); - this.tree.setInput(this.scmViewService, this.loadTreeViewState()); + this.tree.setInput(this.scmViewService, this.loadTreeViewState()).then(() => { + this._viewModel = this._register(this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer)); - this.updateIndentStyles(this.themeService.getFileIconTheme()); - this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); - this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); + this.listContainer.classList.add('file-icon-themable-tree'); + this.listContainer.classList.add('show-file-icons'); + + this.updateIndentStyles(this.themeService.getFileIconTheme()); + this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); + this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); - this._register(this.onDidChangeBodyVisibility(this._viewModel.setVisible, this._viewModel)); + this._register(this.onDidChangeBodyVisibility(this._viewModel.setVisible, this._viewModel)); + this._viewModel.setVisible(this.isBodyVisible()); - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.disposables)(this.updateActions, this)); - this.updateActions(); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.disposables)(this.updateActions, this)); + this.updateActions(); + }); } private updateIndentStyles(theme: IFileIconTheme): void { From 56b4d0a3083cf79e6191061ef3f1143e12b9a9d0 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 20 Oct 2023 23:08:05 +0200 Subject: [PATCH 04/26] Adopt the Sequencer --- .../contrib/scm/browser/scmViewPane.ts | 83 ++++++------------- 1 file changed, 26 insertions(+), 57 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 1ee5618c1ec30..6ec611c622a33 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -26,7 +26,7 @@ import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeSer import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService } from './util'; import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { disposableTimeout, ThrottledDelayer } from 'vs/base/common/async'; +import { disposableTimeout, ThrottledDelayer, Sequencer } from 'vs/base/common/async'; import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { ResourceTree, IResourceNode } from 'vs/base/common/resourceTree'; import { ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/tree/objectTree'; @@ -1123,7 +1123,7 @@ class ViewModel { // Update sort key based on view mode this.sortKey = this.getViewModelSortKey(); - this.tree.updateChildren(); + this._updateChildren(); this._onDidChangeMode.fire(mode); this.modeContextKey.set(mode); @@ -1138,7 +1138,7 @@ class ViewModel { this._sortKey = sortKey; - this.tree.updateChildren(); + this._updateChildren(); this._onDidChangeSortKey.fire(sortKey); this.sortKeyContextKey.set(sortKey); @@ -1157,6 +1157,7 @@ class ViewModel { private readonly visibilityDisposables = new DisposableStore(); private scrollTop: number | undefined; private firstVisible = true; + private readonly updateChildrenSequencer = new Sequencer(); private readonly disposables = new DisposableStore(); private modeContextKey: IContextKey; @@ -1223,18 +1224,20 @@ class ViewModel { this._alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories'); this._showActionButton = this.configurationService.getValue('scm.showActionButton'); - this.tree.updateChildren(); + this._updateChildren(); this.updateRepositoryContextKeys(); } } private _onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void { + this._updateChildren(); + for (const repository of added) { const repositoryDisposables = new DisposableStore(); - repositoryDisposables.add(repository.provider.onDidChange(() => this.tree.updateChildren(repository, true, true))); - repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(() => this.tree.updateChildren(repository, true))); - repositoryDisposables.add(repository.input.onDidChangeVisibility(() => this.tree.updateChildren(repository))); + repositoryDisposables.add(repository.provider.onDidChange(() => this._updateChildren(repository, true, true))); + repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(() => this._updateChildren(repository, true))); + repositoryDisposables.add(repository.input.onDidChangeVisibility(() => this._updateChildren(repository))); const resourceGroupDisposables = repositoryDisposables.add(new DisposableMap()); @@ -1249,8 +1252,8 @@ class ViewModel { if (!resourceGroupDisposables.has(resourceGroup)) { const disposableStore = new DisposableStore(); - disposableStore.add(resourceGroup.onDidChange(() => this.tree.updateChildren(repository))); - disposableStore.add(resourceGroup.onDidChangeResources(() => this.tree.updateChildren(resourceGroup))); + disposableStore.add(resourceGroup.onDidChange(() => this._updateChildren(repository))); + disposableStore.add(resourceGroup.onDidChangeResources(() => this._updateChildren(repository))); resourceGroupDisposables.set(resourceGroup, disposableStore); } } @@ -1265,10 +1268,6 @@ class ViewModel { for (const repository of removed) { this.items.deleteAndDispose(repository); } - - this.tree.updateChildren(); - this.updateRepositoryContextKeys(); - this.updateRepositoryCollapseAllContextKeys(); } setVisible(visible: boolean): void { @@ -1294,50 +1293,20 @@ class ViewModel { this.updateRepositoryCollapseAllContextKeys(); } - // private async refresh(item?: ISCMRepository | ISCMResourceGroup): Promise { - // if (!this.alwaysShowRepositories && this.items.size === 1) { - // const provider = Iterable.first(this.items.keys())!.provider; - // this.scmProviderContextKey.set(provider.contextValue); - // this.scmProviderRootUriContextKey.set(provider.rootUri?.toString()); - // this.scmProviderHasRootUriContextKey.set(!!provider.rootUri); - // } else { - // this.scmProviderContextKey.set(undefined); - // this.scmProviderRootUriContextKey.set(undefined); - // this.scmProviderHasRootUriContextKey.set(false); - // } - - // const focusedInput = this.inputRenderer.getFocusedInput(); - - // if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isSCMRepository(item)))) { - // // Single repository and not always show repositories - // await this.tree.setInput(Iterable.first(this.items.keys())!); - // } else if (item) { - // // Particular repository or resource group - // await this.tree.updateChildren(item); - // } else { - // // Expand repository nodes - // // const expanded = Array.from(this.items.keys()) - // // .map(repository => `repo:${repository.provider.id}`); - - // // await this.tree.setInput([...this.items.keys()], { expanded }); - // } - - // // if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isRepositoryItem(item)))) { - // // const item = Iterable.first(this.items.values())!; - // // this.tree.setChildren(null, this.render(item, this.treeViewState).children); - // // } else if (item) { - // // this.tree.setChildren(item.element, this.render(item, this.treeViewState).children); - // // } else { - // // const items = coalesce(this.scmViewService.visibleRepositories.map(r => this.items.get(r))); - // // this.tree.setChildren(null, items.map(item => this.render(item, this.treeViewState))); - // // } - - // if (focusedInput) { - // this.inputRenderer.getRenderedInputWidget(focusedInput)?.focus(); - // } - - // this.updateRepositoryCollapseAllContextKeys(); - // } + private _updateChildren(element?: ISCMRepository | ISCMResourceGroup, recursive?: boolean, rerender?: boolean) { + this.updateChildrenSequencer.queue(async () => { + this.updateRepositoryContextKeys(); + const focusedInput = this.inputRenderer.getFocusedInput(); + + await this.tree.updateChildren(element, recursive, rerender); + + if (focusedInput) { + this.inputRenderer.getRenderedInputWidget(focusedInput)?.focus(); + } + + this.updateRepositoryCollapseAllContextKeys(); + }); + } private onDidActiveEditorChange(): void { if (!this.configurationService.getValue('scm.autoReveal')) { From 046d44eb8da2160b41221f2551bc3ce927b9e25e Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Sat, 21 Oct 2023 14:52:53 +0200 Subject: [PATCH 05/26] Honour always show repositories setting --- .../contrib/scm/browser/scmViewPane.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 6ec611c622a33..d23dd6d519046 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2456,7 +2456,7 @@ export class SCMViewPane extends ViewPane { } private loadTreeViewState(): IAsyncDataTreeViewState | undefined { - const storageViewState = this.storageService.get(`scm.viewState`, StorageScope.WORKSPACE); + const storageViewState = this.storageService.get('scm.viewState2', StorageScope.WORKSPACE); if (!storageViewState) { return undefined; } @@ -2470,7 +2470,7 @@ export class SCMViewPane extends ViewPane { } private storeTreeViewState() { - this.storageService.store(`scm.viewState`, JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE, StorageTarget.MACHINE); + this.storageService.store('scm.viewState2', JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE, StorageTarget.MACHINE); } override shouldShowWelcome(): boolean { @@ -2514,35 +2514,35 @@ class SCMTreeDataSource implements IAsyncDataSource | Promise> { - if (isSCMViewService(inputOrElement)) { + if (isSCMViewService(inputOrElement) && (this.viewModel().alwaysShowRepositories || this.scmViewService.visibleRepositories.length > 1)) { return this.scmViewService.visibleRepositories; - } - - if (isSCMRepository(inputOrElement)) { + } else if (isSCMViewService(inputOrElement) || isSCMRepository(inputOrElement)) { const children: TreeElement[] = []; - const provider = inputOrElement.provider; + const repository = isSCMViewService(inputOrElement) ? + inputOrElement.visibleRepositories[0] : inputOrElement; + const showActionButton = this.viewModel().showActionButton; const repositoryCount = this.scmViewService.visibleRepositories.length; // SCM Input - if (inputOrElement.input.visible) { - children.push(inputOrElement.input); + if (repository.input.visible) { + children.push(repository.input); } // Action Button - if (showActionButton && provider.actionButton) { + if (showActionButton && repository.provider.actionButton) { children.push({ type: 'actionButton', - repository: inputOrElement, - button: provider.actionButton + repository: repository, + button: repository.provider.actionButton } as ISCMActionButton); } // ResourceGroups - const hasSomeChanges = provider.groups.some(item => item.resources.length > 0); - if (hasSomeChanges || (repositoryCount === 1 && (!showActionButton || !provider.actionButton))) { - children.push(...provider.groups); + const hasSomeChanges = repository.provider.groups.some(item => item.resources.length > 0); + if (hasSomeChanges || (repositoryCount === 1 && (!showActionButton || !repository.provider.actionButton))) { + children.push(...repository.provider.groups); } return children; From c1bed04922c89cc4b2b7968feda46047f9459b83 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 24 Oct 2023 20:37:49 +0200 Subject: [PATCH 06/26] Revert changes related to alwaysShowRepositories --- .../contrib/scm/browser/scmViewPane.ts | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index d23dd6d519046..ca6292f42268a 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2514,35 +2514,33 @@ class SCMTreeDataSource implements IAsyncDataSource | Promise> { - if (isSCMViewService(inputOrElement) && (this.viewModel().alwaysShowRepositories || this.scmViewService.visibleRepositories.length > 1)) { + if (isSCMViewService(inputOrElement)) { return this.scmViewService.visibleRepositories; - } else if (isSCMViewService(inputOrElement) || isSCMRepository(inputOrElement)) { + } else if (isSCMRepository(inputOrElement)) { const children: TreeElement[] = []; - const repository = isSCMViewService(inputOrElement) ? - inputOrElement.visibleRepositories[0] : inputOrElement; - + const provider = inputOrElement.provider; const showActionButton = this.viewModel().showActionButton; const repositoryCount = this.scmViewService.visibleRepositories.length; // SCM Input - if (repository.input.visible) { - children.push(repository.input); + if (inputOrElement.input.visible) { + children.push(inputOrElement.input); } // Action Button - if (showActionButton && repository.provider.actionButton) { + if (showActionButton && provider.actionButton) { children.push({ type: 'actionButton', - repository: repository, - button: repository.provider.actionButton + repository: inputOrElement, + button: provider.actionButton } as ISCMActionButton); } // ResourceGroups - const hasSomeChanges = repository.provider.groups.some(item => item.resources.length > 0); - if (hasSomeChanges || (repositoryCount === 1 && (!showActionButton || !repository.provider.actionButton))) { - children.push(...repository.provider.groups); + const hasSomeChanges = provider.groups.some(item => item.resources.length > 0); + if (hasSomeChanges || (repositoryCount === 1 && (!showActionButton || !provider.actionButton))) { + children.push(...provider.groups); } return children; From 3815c8288cfde3d081ccd6327ff432629fcf7edb Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:27:37 +0200 Subject: [PATCH 07/26] Ad a tactical check for the viewModel existence --- .../contrib/scm/browser/scmViewPane.ts | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index ca6292f42268a..a6b793e699b8a 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1235,9 +1235,9 @@ class ViewModel { for (const repository of added) { const repositoryDisposables = new DisposableStore(); - repositoryDisposables.add(repository.provider.onDidChange(() => this._updateChildren(repository, true, true))); - repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(() => this._updateChildren(repository, true))); - repositoryDisposables.add(repository.input.onDidChangeVisibility(() => this._updateChildren(repository))); + repositoryDisposables.add(repository.provider.onDidChange(() => this._updateChildren())); + repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(() => this._updateChildren())); + repositoryDisposables.add(repository.input.onDidChangeVisibility(() => this._updateChildren())); const resourceGroupDisposables = repositoryDisposables.add(new DisposableMap()); @@ -1252,8 +1252,8 @@ class ViewModel { if (!resourceGroupDisposables.has(resourceGroup)) { const disposableStore = new DisposableStore(); - disposableStore.add(resourceGroup.onDidChange(() => this._updateChildren(repository))); - disposableStore.add(resourceGroup.onDidChangeResources(() => this._updateChildren(repository))); + disposableStore.add(resourceGroup.onDidChange(() => this._updateChildren())); + disposableStore.add(resourceGroup.onDidChangeResources(() => this._updateChildren())); resourceGroupDisposables.set(resourceGroup, disposableStore); } } @@ -2196,6 +2196,8 @@ export class SCMViewPane extends ViewPane { // List this.listContainer = append(container, $('.scm-view.show-file-icons')); + this.listContainer.classList.add('file-icon-themable-tree'); + this.listContainer.classList.add('show-file-icons'); const overflowWidgetsDomNode = $('.scm-overflow-widgets-container.monaco-editor'); @@ -2274,9 +2276,6 @@ export class SCMViewPane extends ViewPane { this.tree.setInput(this.scmViewService, this.loadTreeViewState()).then(() => { this._viewModel = this._register(this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer)); - this.listContainer.classList.add('file-icon-themable-tree'); - this.listContainer.classList.add('show-file-icons'); - this.updateIndentStyles(this.themeService.getFileIconTheme()); this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); @@ -2490,7 +2489,7 @@ export class SCMViewPane extends ViewPane { class SCMTreeDataSource implements IAsyncDataSource { constructor( - private readonly viewModel: () => ViewModel, + private readonly viewModel: () => ViewModel | undefined, @ISCMViewService private readonly scmViewService: ISCMViewService) { } hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean { @@ -2514,13 +2513,17 @@ class SCMTreeDataSource implements IAsyncDataSource | Promise> { + if (this.viewModel() === undefined) { + return []; + } + if (isSCMViewService(inputOrElement)) { return this.scmViewService.visibleRepositories; } else if (isSCMRepository(inputOrElement)) { const children: TreeElement[] = []; const provider = inputOrElement.provider; - const showActionButton = this.viewModel().showActionButton; + const showActionButton = this.viewModel()!.showActionButton; const repositoryCount = this.scmViewService.visibleRepositories.length; // SCM Input @@ -2545,10 +2548,10 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Wed, 1 Nov 2023 13:34:58 +0100 Subject: [PATCH 08/26] Honour the scm.alwaysShowRepositories setting --- .../contrib/scm/browser/scmViewPane.ts | 117 +++++++++--------- 1 file changed, 60 insertions(+), 57 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index a6b793e699b8a..71df0fe17d78f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1196,9 +1196,6 @@ class ViewModel { this.scmProviderRootUriContextKey = ContextKeys.SCMProviderRootUri.bindTo(contextKeyService); this.scmProviderHasRootUriContextKey = ContextKeys.SCMProviderHasRootUri.bindTo(contextKeyService); - configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); - this.onDidChangeConfiguration(); - Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element), this.disposables) (this.updateRepositoryCollapseAllContextKeys, this, this.disposables); @@ -1217,6 +1214,17 @@ class ViewModel { break; } })); + + this.disposables.add(this.storageService.onWillSaveState(e => { + if (e.reason === WillSaveStateReason.SHUTDOWN) { + this.storeTreeViewState(); + } + })); + + this.tree.setInput(this.scmViewService, this.loadTreeViewState()).then(() => { + configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); + this.onDidChangeConfiguration(); + }); } private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { @@ -1225,13 +1233,11 @@ class ViewModel { this._showActionButton = this.configurationService.getValue('scm.showActionButton'); this._updateChildren(); - this.updateRepositoryContextKeys(); } } private _onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void { - this._updateChildren(); - + // Added repositories for (const repository of added) { const repositoryDisposables = new DisposableStore(); @@ -1265,9 +1271,12 @@ class ViewModel { this.items.set(repository, repositoryDisposables); } + // Removed repositories for (const repository of removed) { this.items.deleteAndDispose(repository); } + + this._updateChildren(); } setVisible(visible: boolean): void { @@ -1294,6 +1303,10 @@ class ViewModel { } private _updateChildren(element?: ISCMRepository | ISCMResourceGroup, recursive?: boolean, rerender?: boolean) { + if (!this.tree.getInput()) { + return; + } + this.updateChildrenSequencer.queue(async () => { this.updateRepositoryContextKeys(); const focusedInput = this.inputRenderer.getFocusedInput(); @@ -1443,6 +1456,24 @@ class ViewModel { return viewSortKey; } + private loadTreeViewState(): IAsyncDataTreeViewState | undefined { + const storageViewState = this.storageService.get('scm.viewState2', StorageScope.WORKSPACE); + if (!storageViewState) { + return undefined; + } + + try { + const treeViewState = JSON.parse(storageViewState); + return treeViewState; + } catch { + return undefined; + } + } + + private storeTreeViewState() { + this.storageService.store('scm.viewState2', JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + dispose(): void { this.visibilityDisposables.dispose(); this.disposables.dispose(); @@ -2169,7 +2200,6 @@ export class SCMViewPane extends ViewPane { @IContextKeyService contextKeyService: IContextKeyService, @IMenuService private menuService: IMenuService, @IOpenerService openerService: IOpenerService, - @IStorageService private storageService: IStorageService, @ITelemetryService telemetryService: ITelemetryService, ) { super({ ...options, titleMenuId: MenuId.SCMTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); @@ -2183,12 +2213,6 @@ export class SCMViewPane extends ViewPane { this._register(this.instantiationService.createInstance(ScmInputContentProvider)); this._register(Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire())); - - this._register(this.storageService.onWillSaveState(e => { - if (e.reason === WillSaveStateReason.SHUTDOWN) { - this.storeTreeViewState(); - } - })); } protected override renderBody(container: HTMLElement): void { @@ -2273,19 +2297,16 @@ export class SCMViewPane extends ViewPane { this._register(this.instantiationService.createInstance(RepositoryVisibilityActionController)); - this.tree.setInput(this.scmViewService, this.loadTreeViewState()).then(() => { - this._viewModel = this._register(this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer)); + this._viewModel = this._register(this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer)); - this.updateIndentStyles(this.themeService.getFileIconTheme()); - this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); - this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); + this.updateIndentStyles(this.themeService.getFileIconTheme()); + this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); + this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); - this._register(this.onDidChangeBodyVisibility(this._viewModel.setVisible, this._viewModel)); - this._viewModel.setVisible(this.isBodyVisible()); + this._register(this.onDidChangeBodyVisibility(this._viewModel.setVisible, this._viewModel)); - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.disposables)(this.updateActions, this)); - this.updateActions(); - }); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.disposables)(this.updateActions, this)); + this.updateActions(); } private updateIndentStyles(theme: IFileIconTheme): void { @@ -2454,24 +2475,6 @@ export class SCMViewPane extends ViewPane { .filter(r => !!r && !isSCMResourceGroup(r))! as any; } - private loadTreeViewState(): IAsyncDataTreeViewState | undefined { - const storageViewState = this.storageService.get('scm.viewState2', StorageScope.WORKSPACE); - if (!storageViewState) { - return undefined; - } - - try { - const treeViewState = JSON.parse(storageViewState); - return treeViewState; - } catch { - return undefined; - } - } - - private storeTreeViewState() { - this.storageService.store('scm.viewState2', JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - override shouldShowWelcome(): boolean { return this.scmService.repositoryCount === 0; } @@ -2489,7 +2492,7 @@ export class SCMViewPane extends ViewPane { class SCMTreeDataSource implements IAsyncDataSource { constructor( - private readonly viewModel: () => ViewModel | undefined, + private readonly viewModelProvider: () => ViewModel, @ISCMViewService private readonly scmViewService: ISCMViewService) { } hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean { @@ -2513,18 +2516,18 @@ class SCMTreeDataSource implements IAsyncDataSource | Promise> { - if (this.viewModel() === undefined) { - return []; - } + const repositoryCount = this.scmViewService.visibleRepositories.length; + const alwaysShowRepositories = this.viewModelProvider().alwaysShowRepositories; - if (isSCMViewService(inputOrElement)) { + if (isSCMViewService(inputOrElement) && (repositoryCount > 1 || alwaysShowRepositories)) { return this.scmViewService.visibleRepositories; - } else if (isSCMRepository(inputOrElement)) { + } else if ((isSCMViewService(inputOrElement) && repositoryCount === 1 && !alwaysShowRepositories) || isSCMRepository(inputOrElement)) { const children: TreeElement[] = []; - const provider = inputOrElement.provider; - const showActionButton = this.viewModel()!.showActionButton; - const repositoryCount = this.scmViewService.visibleRepositories.length; + inputOrElement = isSCMRepository(inputOrElement) ? inputOrElement : this.scmViewService.visibleRepositories[0]; + const actionButton = inputOrElement.provider.actionButton; + const resourceGroups = inputOrElement.provider.groups; + const showActionButton = this.viewModelProvider().showActionButton; // SCM Input if (inputOrElement.input.visible) { @@ -2532,26 +2535,26 @@ class SCMTreeDataSource implements IAsyncDataSource item.resources.length > 0); - if (hasSomeChanges || (repositoryCount === 1 && (!showActionButton || !provider.actionButton))) { - children.push(...provider.groups); + const hasSomeChanges = resourceGroups.some(group => group.resources.length > 0); + if (hasSomeChanges || (repositoryCount === 1 && (!showActionButton || !actionButton))) { + children.push(...resourceGroups); } return children; } else if (isSCMResourceGroup(inputOrElement)) { - if (this.viewModel()!.mode === ViewModelMode.List) { + if (this.viewModelProvider().mode === ViewModelMode.List) { // Resources (List) return inputOrElement.resources; - } else if (this.viewModel()!.mode === ViewModelMode.Tree) { + } else if (this.viewModelProvider().mode === ViewModelMode.Tree) { // Resources (Tree) return inputOrElement.resourceTree.root.children; } From 07e375283d9e08e7f810b828fcfec454ba531a5d Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 1 Nov 2023 15:36:01 +0100 Subject: [PATCH 09/26] Fix issues related to autoReveal --- .../contrib/scm/browser/scmViewPane.ts | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 71df0fe17d78f..a4c83280debed 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1124,6 +1124,7 @@ class ViewModel { this.sortKey = this.getViewModelSortKey(); this._updateChildren(); + this._onDidActiveEditorChange(); this._onDidChangeMode.fire(mode); this.modeContextKey.set(mode); @@ -1156,8 +1157,7 @@ class ViewModel { private items = new DisposableMap(); private readonly visibilityDisposables = new DisposableStore(); private scrollTop: number | undefined; - private firstVisible = true; - private readonly updateChildrenSequencer = new Sequencer(); + private readonly asyncOperationSequencer = new Sequencer(); private readonly disposables = new DisposableStore(); private modeContextKey: IContextKey; @@ -1242,8 +1242,11 @@ class ViewModel { const repositoryDisposables = new DisposableStore(); repositoryDisposables.add(repository.provider.onDidChange(() => this._updateChildren())); - repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(() => this._updateChildren())); repositoryDisposables.add(repository.input.onDidChangeVisibility(() => this._updateChildren())); + repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(() => { + this._updateChildren(); + this._onDidActiveEditorChange(); + })); const resourceGroupDisposables = repositoryDisposables.add(new DisposableMap()); @@ -1259,7 +1262,10 @@ class ViewModel { const disposableStore = new DisposableStore(); disposableStore.add(resourceGroup.onDidChange(() => this._updateChildren())); - disposableStore.add(resourceGroup.onDidChangeResources(() => this._updateChildren())); + disposableStore.add(resourceGroup.onDidChangeResources(() => { + this._updateChildren(); + this._onDidActiveEditorChange(); + })); resourceGroupDisposables.set(resourceGroup, disposableStore); } } @@ -1277,6 +1283,7 @@ class ViewModel { } this._updateChildren(); + this._onDidActiveEditorChange(); } setVisible(visible: boolean): void { @@ -1289,8 +1296,8 @@ class ViewModel { this.scrollTop = undefined; } - this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); - this.onDidActiveEditorChange(); + this.editorService.onDidActiveEditorChange(this._onDidActiveEditorChange, this, this.visibilityDisposables); + this._onDidActiveEditorChange(); } else { this.visibilityDisposables.clear(); this._onDidChangeVisibleRepositories({ added: Iterable.empty(), removed: [...this.items.keys()] }); @@ -1307,7 +1314,7 @@ class ViewModel { return; } - this.updateChildrenSequencer.queue(async () => { + this.asyncOperationSequencer.queue(async () => { this.updateRepositoryContextKeys(); const focusedInput = this.inputRenderer.getFocusedInput(); @@ -1321,45 +1328,41 @@ class ViewModel { }); } - private onDidActiveEditorChange(): void { + private _onDidActiveEditorChange(): void { if (!this.configurationService.getValue('scm.autoReveal')) { return; } - if (this.firstVisible) { - this.firstVisible = false; - this.visibilityDisposables.add(disposableTimeout(() => this.onDidActiveEditorChange(), 250)); - return; - } - const uri = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); if (!uri) { return; } - for (const repository of this.scmViewService.visibleRepositories) { - const item = this.items.get(repository); + this.asyncOperationSequencer.queue(async () => { + for (const repository of this.scmViewService.visibleRepositories) { + const item = this.items.get(repository); - if (!item) { - continue; - } + if (!item) { + continue; + } - // go backwards from last group - for (let j = repository.provider.groups.length - 1; j >= 0; j--) { - const groupItem = repository.provider.groups[j]; - const resource = this.mode === ViewModelMode.Tree - ? groupItem.resourceTree.getNode(uri)?.element - : groupItem.resources.find(r => this.uriIdentityService.extUri.isEqual(r.sourceUri, uri)); - - if (resource) { - this.tree.reveal(resource); - this.tree.setSelection([resource]); - this.tree.setFocus([resource]); - return; + // go backwards from last group + for (let j = repository.provider.groups.length - 1; j >= 0; j--) { + const groupItem = repository.provider.groups[j]; + const resource = this.mode === ViewModelMode.Tree + ? groupItem.resourceTree.getNode(uri) + : groupItem.resources.find(r => this.uriIdentityService.extUri.isEqual(r.sourceUri, uri)); + + if (resource) { + this.tree.reveal(resource); + this.tree.setSelection([resource]); + this.tree.setFocus([resource]); + return; + } } } - } + }); } focus() { From 03164cd0987568b900cf33e412505fe73c27ab0c Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 1 Nov 2023 17:23:51 +0100 Subject: [PATCH 10/26] Refactor resource rendering and tree data source --- .../contrib/scm/browser/scmViewPane.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index a4c83280debed..54b624235c6aa 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -503,7 +503,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer 0 ? FileKind.FOLDER : FileKind.FILE; + const fileKind = ResourceTree.isResourceNode(resourceOrFolder) ? FileKind.FOLDER : FileKind.FILE; const viewModel = this.viewModelProvider(); const tooltip = !ResourceTree.isResourceNode(resourceOrFolder) && resourceOrFolder.decorations.tooltip || ''; @@ -1351,7 +1351,7 @@ class ViewModel { for (let j = repository.provider.groups.length - 1; j >= 0; j--) { const groupItem = repository.provider.groups[j]; const resource = this.mode === ViewModelMode.Tree - ? groupItem.resourceTree.getNode(uri) + ? groupItem.resourceTree.getNode(uri)?.element : groupItem.resources.find(r => this.uriIdentityService.extUri.isEqual(r.sourceUri, uri)); if (resource) { @@ -2559,11 +2559,21 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Wed, 1 Nov 2023 17:36:58 +0100 Subject: [PATCH 11/26] Temporary fix for timing issues --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 54b624235c6aa..28470afd8eca6 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1221,10 +1221,8 @@ class ViewModel { } })); - this.tree.setInput(this.scmViewService, this.loadTreeViewState()).then(() => { - configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); - this.onDidChangeConfiguration(); - }); + configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); + this.onDidChangeConfiguration(); } private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { @@ -1286,6 +1284,10 @@ class ViewModel { this._onDidActiveEditorChange(); } + setInput(): void { + this.tree.setInput(this.scmViewService, this.loadTreeViewState()); + } + setVisible(visible: boolean): void { if (visible) { this.scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this.visibilityDisposables); @@ -2301,6 +2303,7 @@ export class SCMViewPane extends ViewPane { this._register(this.instantiationService.createInstance(RepositoryVisibilityActionController)); this._viewModel = this._register(this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer)); + this._viewModel.setInput(); this.updateIndentStyles(this.themeService.getFileIconTheme()); this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); From 8bd2a8751518459a46731f9fcb1bf4d492083fb2 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 2 Nov 2023 15:09:59 +0100 Subject: [PATCH 12/26] More refactoring --- .../contrib/scm/browser/scmViewPane.ts | 1328 ++++++++--------- 1 file changed, 633 insertions(+), 695 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index b6dc91e857278..30abb19abcda9 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -25,7 +25,7 @@ import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService } from './util'; import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; -import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, ThrottledDelayer, Sequencer } from 'vs/base/common/async'; import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { ResourceTree, IResourceNode } from 'vs/base/common/resourceTree'; @@ -39,8 +39,7 @@ import { FuzzyScore, createMatches, IMatch } from 'vs/base/common/filters'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; import { flatten } from 'vs/base/common/arrays'; -import { memoize } from 'vs/base/common/decorators'; -import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; @@ -459,7 +458,7 @@ class ResourceRenderer implements ICompressibleTreeRenderer(); constructor( - private viewModelProvider: () => ViewModel, + private viewMode: () => ViewMode, private labels: ResourceLabels, private actionViewItemProvider: IActionViewItemProvider, private actionRunner: ActionRunner, @@ -492,8 +491,8 @@ class ResourceRenderer implements ICompressibleTreeRenderer { export class SCMTreeSorter implements ITreeSorter { - @memoize - private get viewModel(): ViewModel { return this.viewModelProvider(); } - - constructor(private viewModelProvider: () => ViewModel) { } + constructor( + private readonly viewMode: () => ViewMode, + private readonly viewSortKey: () => ViewSortKey) { } compare(one: TreeElement, other: TreeElement): number { if (isSCMRepository(one)) { @@ -778,9 +767,9 @@ export class SCMTreeSorter implements ITreeSorter { } // List - if (this.viewModel.mode === ViewModelMode.List) { + if (this.viewMode() === ViewMode.List) { // FileName - if (this.viewModel.sortKey === ViewModelSortKey.Name) { + if (this.viewSortKey() === ViewSortKey.Name) { const oneName = basename((one as ISCMResource).sourceUri); const otherName = basename((other as ISCMResource).sourceUri); @@ -788,7 +777,7 @@ export class SCMTreeSorter implements ITreeSorter { } // Status - if (this.viewModel.sortKey === ViewModelSortKey.Status) { + if (this.viewSortKey() === ViewSortKey.Status) { const oneTooltip = (one as ISCMResource).decorations.tooltip ?? ''; const otherTooltip = (other as ISCMResource).decorations.tooltip ?? ''; @@ -822,7 +811,7 @@ export class SCMTreeSorter implements ITreeSorter { export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyboardNavigationLabelProvider { constructor( - private viewModelProvider: () => ViewModel, + private viewMode: () => ViewMode, @ILabelService private readonly labelService: ILabelService, ) { } @@ -834,8 +823,7 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb } else if (isSCMResourceGroup(element)) { return element.label; } else { - const viewModel = this.viewModelProvider(); - if (viewModel.mode === ViewModelMode.List) { + if (this.viewMode() === ViewMode.List) { // In List mode match using the file name and the path. // Since we want to match both on the file name and the // full path we return an array of labels. A match in the @@ -939,12 +927,12 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider('scmViewModelMode', ViewModelMode.List), - ViewModelSortKey: new RawContextKey('scmViewModelSortKey', ViewModelSortKey.Path), - ViewModelAreAllRepositoriesCollapsed: new RawContextKey('scmViewModelAreAllRepositoriesCollapsed', false), - ViewModelIsAnyRepositoryCollapsible: new RawContextKey('scmViewModelIsAnyRepositoryCollapsible', false), + SCMViewMode: new RawContextKey('scmViewMode', ViewMode.List), + SCMViewSortKey: new RawContextKey('scmViewSortKey', ViewSortKey.Path), + SCMViewAreAllRepositoriesCollapsed: new RawContextKey('scmViewAreAllRepositoriesCollapsed', false), + SCMViewIsAnyRepositoryCollapsible: new RawContextKey('scmViewIsAnyRepositoryCollapsible', false), SCMProvider: new RawContextKey('scmProvider', undefined), SCMProviderRootUri: new RawContextKey('scmProviderRootUri', undefined), SCMProviderHasRootUri: new RawContextKey('scmProviderHasRootUri', undefined), @@ -1090,633 +1078,248 @@ class RepositoryVisibilityActionController { } } -class ViewModel { - - private readonly _onDidChangeMode = new Emitter(); - readonly onDidChangeMode = this._onDidChangeMode.event; +class SetListViewModeAction extends ViewAction { + constructor(menu: Partial = {}) { + super({ + id: 'workbench.scm.action.setListViewMode', + title: localize('setListViewMode', "View as List"), + viewId: VIEW_PANE_ID, + f1: false, + icon: Codicon.listTree, + toggled: ContextKeys.SCMViewMode.isEqualTo(ViewMode.List), + menu: { id: Menus.ViewSort, group: '1_viewmode', ...menu } + }); + } - private readonly _onDidChangeSortKey = new Emitter(); - readonly onDidChangeSortKey = this._onDidChangeSortKey.event; + async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { + view.viewMode = ViewMode.List; + } +} - private visible: boolean = false; +class SetListViewModeNavigationAction extends SetListViewModeAction { + constructor() { + super({ + id: MenuId.SCMTitle, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeys.SCMViewMode.isEqualTo(ViewMode.Tree)), + group: 'navigation', + order: -1000 + }); + } +} - get mode(): ViewModelMode { return this._mode; } - set mode(mode: ViewModelMode) { - if (this._mode === mode) { - return; - } +class SetTreeViewModeAction extends ViewAction { + constructor(menu: Partial = {}) { + super({ + id: 'workbench.scm.action.setTreeViewMode', + title: localize('setTreeViewMode', "View as Tree"), + viewId: VIEW_PANE_ID, + f1: false, + icon: Codicon.listFlat, + toggled: ContextKeys.SCMViewMode.isEqualTo(ViewMode.Tree), + menu: { id: Menus.ViewSort, group: '1_viewmode', ...menu } + }); + } - this._mode = mode; + async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { + view.viewMode = ViewMode.Tree; + } +} - // Update sort key based on view mode - this.sortKey = this.getViewModelSortKey(); +class SetTreeViewModeNavigationAction extends SetTreeViewModeAction { + constructor() { + super({ + id: MenuId.SCMTitle, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeys.SCMViewMode.isEqualTo(ViewMode.List)), + group: 'navigation', + order: -1000 + }); + } +} - this._updateChildren(); - this._onDidActiveEditorChange(); - this._onDidChangeMode.fire(mode); - this.modeContextKey.set(mode); +registerAction2(SetListViewModeAction); +registerAction2(SetTreeViewModeAction); +registerAction2(SetListViewModeNavigationAction); +registerAction2(SetTreeViewModeNavigationAction); - this.storageService.store(`scm.viewMode`, mode, StorageScope.WORKSPACE, StorageTarget.USER); +abstract class RepositorySortAction extends ViewAction { + constructor(private sortKey: ISCMRepositorySortKey, title: string) { + super({ + id: `workbench.scm.action.repositories.setSortKey.${sortKey}`, + title, + viewId: VIEW_PANE_ID, + f1: false, + toggled: RepositoryContextKeys.RepositorySortKey.isEqualTo(sortKey), + menu: [ + { + id: Menus.Repositories, + group: '1_sort' + }, + { + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', REPOSITORIES_VIEW_PANE_ID), + group: '1_sort', + }, + ] + }); } - get sortKey(): ViewModelSortKey { return this._sortKey; } - set sortKey(sortKey: ViewModelSortKey) { - if (this._sortKey === sortKey) { - return; - } + runInView(accessor: ServicesAccessor) { + accessor.get(ISCMViewService).toggleSortKey(this.sortKey); + } +} - this._sortKey = sortKey; - this._updateChildren(); - this._onDidChangeSortKey.fire(sortKey); - this.sortKeyContextKey.set(sortKey); +class RepositorySortByDiscoveryTimeAction extends RepositorySortAction { + constructor() { + super(ISCMRepositorySortKey.DiscoveryTime, localize('repositorySortByDiscoveryTime', "Sort by Discovery Time")); + } +} - if (this._mode === ViewModelMode.List) { - this.storageService.store(`scm.viewSortKey`, sortKey, StorageScope.WORKSPACE, StorageTarget.USER); - } +class RepositorySortByNameAction extends RepositorySortAction { + constructor() { + super(ISCMRepositorySortKey.Name, localize('repositorySortByName', "Sort by Name")); } +} - private _showActionButton = false; - get showActionButton(): boolean { return this._showActionButton; } +class RepositorySortByPathAction extends RepositorySortAction { + constructor() { + super(ISCMRepositorySortKey.Path, localize('repositorySortByPath', "Sort by Path")); + } +} - private _alwaysShowRepositories = false; - get alwaysShowRepositories(): boolean { return this._alwaysShowRepositories; } +registerAction2(RepositorySortByDiscoveryTimeAction); +registerAction2(RepositorySortByNameAction); +registerAction2(RepositorySortByPathAction); - private items = new DisposableMap(); - private readonly visibilityDisposables = new DisposableStore(); - private scrollTop: number | undefined; - private readonly asyncOperationSequencer = new Sequencer(); - private readonly disposables = new DisposableStore(); +abstract class SetSortKeyAction extends ViewAction { + constructor(private sortKey: ViewSortKey, title: string) { + super({ + id: `workbench.scm.action.setSortKey.${sortKey}`, + title, + viewId: VIEW_PANE_ID, + f1: false, + toggled: ContextKeys.SCMViewSortKey.isEqualTo(sortKey), + precondition: ContextKeys.SCMViewMode.isEqualTo(ViewMode.List), + menu: { id: Menus.ViewSort, group: '2_sort' } + }); + } - private modeContextKey: IContextKey; - private sortKeyContextKey: IContextKey; - private areAllRepositoriesCollapsedContextKey: IContextKey; - private isAnyRepositoryCollapsibleContextKey: IContextKey; - private scmProviderContextKey: IContextKey; - private scmProviderRootUriContextKey: IContextKey; - private scmProviderHasRootUriContextKey: IContextKey; + async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { + view.viewSortKey = this.sortKey; + } +} - private _mode: ViewModelMode; - private _sortKey: ViewModelSortKey; +class SetSortByNameAction extends SetSortKeyAction { + constructor() { + super(ViewSortKey.Name, localize('sortChangesByName', "Sort Changes by Name")); + } +} - constructor( - private tree: WorkbenchCompressibleAsyncDataTree, - private inputRenderer: InputRenderer, - @IInstantiationService protected instantiationService: IInstantiationService, - @IEditorService protected editorService: IEditorService, - @IConfigurationService protected configurationService: IConfigurationService, - @ISCMViewService private scmViewService: ISCMViewService, - @IStorageService private storageService: IStorageService, - @IUriIdentityService private uriIdentityService: IUriIdentityService, - @IContextKeyService contextKeyService: IContextKeyService - ) { - // View mode and sort key - this._mode = this.getViewModelMode(); - this._sortKey = this.getViewModelSortKey(); - - this.modeContextKey = ContextKeys.ViewModelMode.bindTo(contextKeyService); - this.modeContextKey.set(this._mode); - this.sortKeyContextKey = ContextKeys.ViewModelSortKey.bindTo(contextKeyService); - this.sortKeyContextKey.set(this._sortKey); - this.areAllRepositoriesCollapsedContextKey = ContextKeys.ViewModelAreAllRepositoriesCollapsed.bindTo(contextKeyService); - this.isAnyRepositoryCollapsibleContextKey = ContextKeys.ViewModelIsAnyRepositoryCollapsible.bindTo(contextKeyService); - this.scmProviderContextKey = ContextKeys.SCMProvider.bindTo(contextKeyService); - this.scmProviderRootUriContextKey = ContextKeys.SCMProviderRootUri.bindTo(contextKeyService); - this.scmProviderHasRootUriContextKey = ContextKeys.SCMProviderHasRootUri.bindTo(contextKeyService); +class SetSortByPathAction extends SetSortKeyAction { + constructor() { + super(ViewSortKey.Path, localize('sortChangesByPath', "Sort Changes by Path")); + } +} - Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element), this.disposables) - (this.updateRepositoryCollapseAllContextKeys, this, this.disposables); +class SetSortByStatusAction extends SetSortKeyAction { + constructor() { + super(ViewSortKey.Status, localize('sortChangesByStatus', "Sort Changes by Status")); + } +} - this.disposables.add(this.storageService.onWillSaveState(e => { - this.mode = this.getViewModelMode(); - this.sortKey = this.getViewModelSortKey(); - })); +registerAction2(SetSortByNameAction); +registerAction2(SetSortByPathAction); +registerAction2(SetSortByStatusAction); - this.disposables.add(this.storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, this.disposables)(e => { - switch (e.key) { - case 'scm.viewMode': - this.mode = this.getViewModelMode(); - break; - case 'scm.viewSortKey': - this.sortKey = this.getViewModelSortKey(); - break; - } - })); +class CollapseAllRepositoriesAction extends ViewAction { - this.disposables.add(this.storageService.onWillSaveState(e => { - if (e.reason === WillSaveStateReason.SHUTDOWN) { - this.storeTreeViewState(); + constructor() { + super({ + id: `workbench.scm.action.collapseAllRepositories`, + title: localize('collapse all', "Collapse All Repositories"), + viewId: VIEW_PANE_ID, + f1: false, + icon: Codicon.collapseAll, + menu: { + id: MenuId.SCMTitle, + group: 'navigation', + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.SCMViewIsAnyRepositoryCollapsible.isEqualTo(true), ContextKeys.SCMViewAreAllRepositoriesCollapsed.isEqualTo(false)) } - })); + }); + } - configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); - this.onDidChangeConfiguration(); + async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { + view.collapseAllRepositories(); } +} - private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { - if (!e || e.affectsConfiguration('scm.alwaysShowRepositories') || e.affectsConfiguration('scm.showActionButton')) { - this._alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories'); - this._showActionButton = this.configurationService.getValue('scm.showActionButton'); +class ExpandAllRepositoriesAction extends ViewAction { - this._updateChildren(); - } + constructor() { + super({ + id: `workbench.scm.action.expandAllRepositories`, + title: localize('expand all', "Expand All Repositories"), + viewId: VIEW_PANE_ID, + f1: false, + icon: Codicon.expandAll, + menu: { + id: MenuId.SCMTitle, + group: 'navigation', + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.SCMViewIsAnyRepositoryCollapsible.isEqualTo(true), ContextKeys.SCMViewAreAllRepositoriesCollapsed.isEqualTo(true)) + } + }); } - private _onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void { - // Added repositories - for (const repository of added) { - const repositoryDisposables = new DisposableStore(); + async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { + view.expandAllRepositories(); + } +} - repositoryDisposables.add(repository.provider.onDidChange(() => this._updateChildren())); - repositoryDisposables.add(repository.input.onDidChangeActionButton(() => this._updateChildren())); - repositoryDisposables.add(repository.input.onDidChangeVisibility(() => this._updateChildren())); - repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(() => { - this._updateChildren(); - this._onDidActiveEditorChange(); - })); +registerAction2(CollapseAllRepositoriesAction); +registerAction2(ExpandAllRepositoriesAction); - const resourceGroupDisposables = repositoryDisposables.add(new DisposableMap()); +class SCMInputWidget { - const onDidChangeResourceGroups = () => { - for (const [resourceGroup] of resourceGroupDisposables) { - if (!repository.provider.groups.includes(resourceGroup)) { - resourceGroupDisposables.deleteAndDispose(resourceGroup); - } - } + private static readonly ValidationTimeouts: { [severity: number]: number } = { + [InputValidationType.Information]: 5000, + [InputValidationType.Warning]: 8000, + [InputValidationType.Error]: 10000 + }; - for (const resourceGroup of repository.provider.groups) { - if (!resourceGroupDisposables.has(resourceGroup)) { - const disposableStore = new DisposableStore(); + private readonly defaultInputFontFamily = DEFAULT_FONT_FAMILY; - disposableStore.add(resourceGroup.onDidChange(() => this._updateChildren())); - disposableStore.add(resourceGroup.onDidChangeResources(() => { - this._updateChildren(); - this._onDidActiveEditorChange(); - })); - resourceGroupDisposables.set(resourceGroup, disposableStore); - } - } - }; + private element: HTMLElement; + private editorContainer: HTMLElement; + private placeholderTextContainer: HTMLElement; + private inputEditor: CodeEditorWidget; + private toolbarContainer: HTMLElement; + private actionBar: ActionBar; + private readonly disposables = new DisposableStore(); - repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(onDidChangeResourceGroups)); - onDidChangeResourceGroups(); + private model: { readonly input: ISCMInput; textModelRef?: IReference } | undefined; + private repositoryIdContextKey: IContextKey; + private readonly repositoryDisposables = new DisposableStore(); - this.items.set(repository, repositoryDisposables); - } + private validation: IInputValidation | undefined; + private validationDisposable: IDisposable = Disposable.None; + private validationHasFocus: boolean = false; + private _validationTimer: any; - // Removed repositories - for (const repository of removed) { - this.items.deleteAndDispose(repository); - } + // This is due to "Setup height change listener on next tick" above + // https://github.com/microsoft/vscode/issues/108067 + private lastLayoutWasTrash = false; + private shouldFocusAfterLayout = false; - this._updateChildren(); - this._onDidActiveEditorChange(); - } + readonly onDidChangeContentHeight: Event; - setInput(): void { - this.tree.setInput(this.scmViewService, this.loadTreeViewState()); + private get input(): ISCMInput | undefined { + return this.model?.input; } - setVisible(visible: boolean): void { - if (visible) { - this.scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this.visibilityDisposables); - this._onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); - - if (typeof this.scrollTop === 'number') { - this.tree.scrollTop = this.scrollTop; - this.scrollTop = undefined; - } - - this.editorService.onDidActiveEditorChange(this._onDidActiveEditorChange, this, this.visibilityDisposables); - this._onDidActiveEditorChange(); - } else { - this.visibilityDisposables.clear(); - this._onDidChangeVisibleRepositories({ added: Iterable.empty(), removed: [...this.items.keys()] }); - this.scrollTop = this.tree.scrollTop; - } - - this.visible = visible; - this.updateRepositoryContextKeys(); - this.updateRepositoryCollapseAllContextKeys(); - } - - private _updateChildren(element?: ISCMRepository | ISCMResourceGroup, recursive?: boolean, rerender?: boolean) { - if (!this.tree.getInput()) { - return; - } - - this.asyncOperationSequencer.queue(async () => { - this.updateRepositoryContextKeys(); - const focusedInput = this.inputRenderer.getFocusedInput(); - - await this.tree.updateChildren(element, recursive, rerender); - - if (focusedInput) { - this.inputRenderer.getRenderedInputWidget(focusedInput)?.focus(); - } - - this.updateRepositoryCollapseAllContextKeys(); - }); - } - - private _onDidActiveEditorChange(): void { - if (!this.configurationService.getValue('scm.autoReveal')) { - return; - } - - const uri = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); - - if (!uri) { - return; - } - - this.asyncOperationSequencer.queue(async () => { - for (const repository of this.scmViewService.visibleRepositories) { - const item = this.items.get(repository); - - if (!item) { - continue; - } - - // go backwards from last group - for (let j = repository.provider.groups.length - 1; j >= 0; j--) { - const groupItem = repository.provider.groups[j]; - const resource = this.mode === ViewModelMode.Tree - ? groupItem.resourceTree.getNode(uri)?.element - : groupItem.resources.find(r => this.uriIdentityService.extUri.isEqual(r.sourceUri, uri)); - - if (resource) { - this.tree.reveal(resource); - this.tree.setSelection([resource]); - this.tree.setFocus([resource]); - return; - } - } - } - }); - } - - focus() { - if (this.tree.getFocus().length === 0) { - for (const repository of this.scmViewService.visibleRepositories) { - const widget = this.inputRenderer.getRenderedInputWidget(repository.input); - - if (widget) { - widget.focus(); - return; - } - } - } - - this.tree.domFocus(); - } - - private updateRepositoryContextKeys(): void { - if (!this.alwaysShowRepositories && this.items.size === 1) { - const provider = Iterable.first(this.items.keys())!.provider; - this.scmProviderContextKey.set(provider.contextValue); - this.scmProviderRootUriContextKey.set(provider.rootUri?.toString()); - this.scmProviderHasRootUriContextKey.set(!!provider.rootUri); - } else { - this.scmProviderContextKey.set(undefined); - this.scmProviderRootUriContextKey.set(undefined); - this.scmProviderHasRootUriContextKey.set(false); - } - } - - private updateRepositoryCollapseAllContextKeys(): void { - if (!this.visible || this.scmViewService.visibleRepositories.length === 1) { - this.isAnyRepositoryCollapsibleContextKey.set(false); - this.areAllRepositoriesCollapsedContextKey.set(false); - return; - } - - this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r))); - this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); - } - - collapseAllRepositories(): void { - for (const repository of this.scmViewService.visibleRepositories) { - if (this.tree.isCollapsible(repository)) { - this.tree.collapse(repository); - } - } - } - - expandAllRepositories(): void { - for (const repository of this.scmViewService.visibleRepositories) { - if (this.tree.isCollapsible(repository)) { - this.tree.expand(repository); - } - } - } - - private getViewModelMode(): ViewModelMode { - let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree; - const storageMode = this.storageService.get(`scm.viewMode`, StorageScope.WORKSPACE) as ViewModelMode; - if (typeof storageMode === 'string') { - mode = storageMode; - } - - return mode; - } - - private getViewModelSortKey(): ViewModelSortKey { - // Tree - if (this._mode === ViewModelMode.Tree) { - return ViewModelSortKey.Path; - } - - // List - let viewSortKey: ViewModelSortKey; - const viewSortKeyString = this.configurationService.getValue<'path' | 'name' | 'status'>('scm.defaultViewSortKey'); - switch (viewSortKeyString) { - case 'name': - viewSortKey = ViewModelSortKey.Name; - break; - case 'status': - viewSortKey = ViewModelSortKey.Status; - break; - default: - viewSortKey = ViewModelSortKey.Path; - break; - } - - const storageSortKey = this.storageService.get(`scm.viewSortKey`, StorageScope.WORKSPACE) as ViewModelSortKey; - if (typeof storageSortKey === 'string') { - viewSortKey = storageSortKey; - } - - return viewSortKey; - } - - private loadTreeViewState(): IAsyncDataTreeViewState | undefined { - const storageViewState = this.storageService.get('scm.viewState2', StorageScope.WORKSPACE); - if (!storageViewState) { - return undefined; - } - - try { - const treeViewState = JSON.parse(storageViewState); - return treeViewState; - } catch { - return undefined; - } - } - - private storeTreeViewState() { - this.storageService.store('scm.viewState2', JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - - dispose(): void { - this.visibilityDisposables.dispose(); - this.disposables.dispose(); - this.items.dispose(); - } -} - -class SetListViewModeAction extends ViewAction { - constructor(menu: Partial = {}) { - super({ - id: 'workbench.scm.action.setListViewMode', - title: localize('setListViewMode', "View as List"), - viewId: VIEW_PANE_ID, - f1: false, - icon: Codicon.listTree, - toggled: ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.List), - menu: { id: Menus.ViewSort, group: '1_viewmode', ...menu } - }); - } - - async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { - view.viewModel.mode = ViewModelMode.List; - } -} - -class SetListViewModeNavigationAction extends SetListViewModeAction { - constructor() { - super({ - id: MenuId.SCMTitle, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.Tree)), - group: 'navigation', - order: -1000 - }); - } -} - -class SetTreeViewModeAction extends ViewAction { - constructor(menu: Partial = {}) { - super({ - id: 'workbench.scm.action.setTreeViewMode', - title: localize('setTreeViewMode', "View as Tree"), - viewId: VIEW_PANE_ID, - f1: false, - icon: Codicon.listFlat, - toggled: ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.Tree), - menu: { id: Menus.ViewSort, group: '1_viewmode', ...menu } - }); - } - - async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { - view.viewModel.mode = ViewModelMode.Tree; - } -} - -class SetTreeViewModeNavigationAction extends SetTreeViewModeAction { - constructor() { - super({ - id: MenuId.SCMTitle, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.List)), - group: 'navigation', - order: -1000 - }); - } -} - -registerAction2(SetListViewModeAction); -registerAction2(SetTreeViewModeAction); -registerAction2(SetListViewModeNavigationAction); -registerAction2(SetTreeViewModeNavigationAction); - -abstract class RepositorySortAction extends ViewAction { - constructor(private sortKey: ISCMRepositorySortKey, title: string) { - super({ - id: `workbench.scm.action.repositories.setSortKey.${sortKey}`, - title, - viewId: VIEW_PANE_ID, - f1: false, - toggled: RepositoryContextKeys.RepositorySortKey.isEqualTo(sortKey), - menu: [ - { - id: Menus.Repositories, - group: '1_sort' - }, - { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', REPOSITORIES_VIEW_PANE_ID), - group: '1_sort', - }, - ] - }); - } - - runInView(accessor: ServicesAccessor) { - accessor.get(ISCMViewService).toggleSortKey(this.sortKey); - } -} - - -class RepositorySortByDiscoveryTimeAction extends RepositorySortAction { - constructor() { - super(ISCMRepositorySortKey.DiscoveryTime, localize('repositorySortByDiscoveryTime', "Sort by Discovery Time")); - } -} - -class RepositorySortByNameAction extends RepositorySortAction { - constructor() { - super(ISCMRepositorySortKey.Name, localize('repositorySortByName', "Sort by Name")); - } -} - -class RepositorySortByPathAction extends RepositorySortAction { - constructor() { - super(ISCMRepositorySortKey.Path, localize('repositorySortByPath', "Sort by Path")); - } -} - -registerAction2(RepositorySortByDiscoveryTimeAction); -registerAction2(RepositorySortByNameAction); -registerAction2(RepositorySortByPathAction); - -abstract class SetSortKeyAction extends ViewAction { - constructor(private sortKey: ViewModelSortKey, title: string) { - super({ - id: `workbench.scm.action.setSortKey.${sortKey}`, - title, - viewId: VIEW_PANE_ID, - f1: false, - toggled: ContextKeys.ViewModelSortKey.isEqualTo(sortKey), - precondition: ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.List), - menu: { id: Menus.ViewSort, group: '2_sort' } - }); - } - - async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { - view.viewModel.sortKey = this.sortKey; - } -} - -class SetSortByNameAction extends SetSortKeyAction { - constructor() { - super(ViewModelSortKey.Name, localize('sortChangesByName', "Sort Changes by Name")); - } -} - -class SetSortByPathAction extends SetSortKeyAction { - constructor() { - super(ViewModelSortKey.Path, localize('sortChangesByPath', "Sort Changes by Path")); - } -} - -class SetSortByStatusAction extends SetSortKeyAction { - constructor() { - super(ViewModelSortKey.Status, localize('sortChangesByStatus', "Sort Changes by Status")); - } -} - -registerAction2(SetSortByNameAction); -registerAction2(SetSortByPathAction); -registerAction2(SetSortByStatusAction); - -class CollapseAllRepositoriesAction extends ViewAction { - - constructor() { - super({ - id: `workbench.scm.action.collapseAllRepositories`, - title: localize('collapse all', "Collapse All Repositories"), - viewId: VIEW_PANE_ID, - f1: false, - icon: Codicon.collapseAll, - menu: { - id: MenuId.SCMTitle, - group: 'navigation', - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.ViewModelIsAnyRepositoryCollapsible.isEqualTo(true), ContextKeys.ViewModelAreAllRepositoriesCollapsed.isEqualTo(false)) - } - }); - } - - async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { - view.viewModel.collapseAllRepositories(); - } -} - -class ExpandAllRepositoriesAction extends ViewAction { - - constructor() { - super({ - id: `workbench.scm.action.expandAllRepositories`, - title: localize('expand all', "Expand All Repositories"), - viewId: VIEW_PANE_ID, - f1: false, - icon: Codicon.expandAll, - menu: { - id: MenuId.SCMTitle, - group: 'navigation', - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.ViewModelIsAnyRepositoryCollapsible.isEqualTo(true), ContextKeys.ViewModelAreAllRepositoriesCollapsed.isEqualTo(true)) - } - }); - } - - async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { - view.viewModel.expandAllRepositories(); - } -} - -registerAction2(CollapseAllRepositoriesAction); -registerAction2(ExpandAllRepositoriesAction); - -class SCMInputWidget { - - private static readonly ValidationTimeouts: { [severity: number]: number } = { - [InputValidationType.Information]: 5000, - [InputValidationType.Warning]: 8000, - [InputValidationType.Error]: 10000 - }; - - private readonly defaultInputFontFamily = DEFAULT_FONT_FAMILY; - - private element: HTMLElement; - private editorContainer: HTMLElement; - private placeholderTextContainer: HTMLElement; - private inputEditor: CodeEditorWidget; - private toolbarContainer: HTMLElement; - private actionBar: ActionBar; - private readonly disposables = new DisposableStore(); - - private model: { readonly input: ISCMInput; textModelRef?: IReference } | undefined; - private repositoryIdContextKey: IContextKey; - private readonly repositoryDisposables = new DisposableStore(); - - private validation: IInputValidation | undefined; - private validationDisposable: IDisposable = Disposable.None; - private validationHasFocus: boolean = false; - private _validationTimer: any; - - // This is due to "Setup height change listener on next tick" above - // https://github.com/microsoft/vscode/issues/108067 - private lastLayoutWasTrash = false; - private shouldFocusAfterLayout = false; - - readonly onDidChangeContentHeight: Event; - - private get input(): ISCMInput | undefined { - return this.model?.input; - } - - public async setInput(input: ISCMInput | undefined) { - if (input === this.input) { - return; - } + public async setInput(input: ISCMInput | undefined) { + if (input === this.input) { + return; + } this.clearValidation(); this.element.classList.remove('synthetic-focus'); @@ -2223,40 +1826,135 @@ export class SCMViewPane extends ViewPane { private _onDidLayout: Emitter; private layoutCache: ISCMLayout; - private listContainer!: HTMLElement; + private treeScrollTop: number | undefined; + private treeContainer!: HTMLElement; private tree!: WorkbenchCompressibleAsyncDataTree; - private _viewModel!: ViewModel; - get viewModel(): ViewModel { return this._viewModel; } + private listLabels!: ResourceLabels; private inputRenderer!: InputRenderer; private actionButtonRenderer!: ActionButtonRenderer; + + private _viewMode: ViewMode; + get viewMode(): ViewMode { return this._viewMode; } + set viewMode(mode: ViewMode) { + if (this._viewMode === mode) { + return; + } + + this._viewMode = mode; + + // Update sort key based on view mode + this.viewSortKey = this.getViewSortKey(); + + this.updateChildren(); + this.onDidActiveEditorChange(); + this._onDidChangeViewMode.fire(mode); + this.viewModeContextKey.set(mode); + + this.updateIndentStyles(this.themeService.getFileIconTheme()); + this.storageService.store(`scm.viewMode`, mode, StorageScope.WORKSPACE, StorageTarget.USER); + } + + private readonly _onDidChangeViewMode = new Emitter(); + readonly onDidChangeViewMode = this._onDidChangeViewMode.event; + + private _viewSortKey: ViewSortKey; + get viewSortKey(): ViewSortKey { return this._viewSortKey; } + set viewSortKey(sortKey: ViewSortKey) { + if (this._viewSortKey === sortKey) { + return; + } + + this._viewSortKey = sortKey; + + this.updateChildren(); + this.viewSortKeyContextKey.set(sortKey); + this._onDidChangeViewSortKey.fire(sortKey); + + if (this._viewMode === ViewMode.List) { + this.storageService.store(`scm.viewSortKey`, sortKey, StorageScope.WORKSPACE, StorageTarget.USER); + } + } + + private readonly _onDidChangeViewSortKey = new Emitter(); + readonly onDidChangeViewSortKey = this._onDidChangeViewSortKey.event; + + private _showActionButton = false; + get showActionButton(): boolean { return this._showActionButton; } + + private _alwaysShowRepositories = false; + get alwaysShowRepositories(): boolean { return this._alwaysShowRepositories; } + + private readonly items = new DisposableMap(); + private readonly visibilityDisposables = new DisposableStore(); + private readonly asyncOperationSequencer = new Sequencer(); + + private viewModeContextKey: IContextKey; + private viewSortKeyContextKey: IContextKey; + private areAllRepositoriesCollapsedContextKey: IContextKey; + private isAnyRepositoryCollapsibleContextKey: IContextKey; + private scmProviderContextKey: IContextKey; + private scmProviderRootUriContextKey: IContextKey; + private scmProviderHasRootUriContextKey: IContextKey; + private readonly disposables = new DisposableStore(); constructor( options: IViewPaneOptions, - @ISCMService private scmService: ISCMService, - @ISCMViewService private scmViewService: ISCMViewService, + @ISCMService private readonly scmService: ISCMService, + @ISCMViewService private readonly scmViewService: ISCMViewService, @IKeybindingService keybindingService: IKeybindingService, @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, - @ICommandService private commandService: ICommandService, - @IEditorService private editorService: IEditorService, + @ICommandService private readonly commandService: ICommandService, + @IEditorService private readonly editorService: IEditorService, @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, - @IMenuService private menuService: IMenuService, + @IMenuService private readonly menuService: IMenuService, + @IStorageService private readonly storageService: IStorageService, @IOpenerService openerService: IOpenerService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ITelemetryService telemetryService: ITelemetryService, ) { super({ ...options, titleMenuId: MenuId.SCMTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + // View mode and sort key + this._viewMode = this.getViewMode(); + this._viewSortKey = this.getViewSortKey(); + + // Context Keys + this.viewModeContextKey = ContextKeys.SCMViewMode.bindTo(contextKeyService); + this.viewModeContextKey.set(this._viewMode); + this.viewSortKeyContextKey = ContextKeys.SCMViewSortKey.bindTo(contextKeyService); + this.viewSortKeyContextKey.set(this.viewSortKey); + this.areAllRepositoriesCollapsedContextKey = ContextKeys.SCMViewAreAllRepositoriesCollapsed.bindTo(contextKeyService); + this.isAnyRepositoryCollapsibleContextKey = ContextKeys.SCMViewIsAnyRepositoryCollapsible.bindTo(contextKeyService); + this.scmProviderContextKey = ContextKeys.SCMProvider.bindTo(contextKeyService); + this.scmProviderRootUriContextKey = ContextKeys.SCMProviderRootUri.bindTo(contextKeyService); + this.scmProviderHasRootUriContextKey = ContextKeys.SCMProviderHasRootUri.bindTo(contextKeyService); + this._onDidLayout = new Emitter(); - this.layoutCache = { - height: undefined, - width: undefined, - onDidChange: this._onDidLayout.event - }; + this.layoutCache = { height: undefined, width: undefined, onDidChange: this._onDidLayout.event }; + + this._register(this.storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, this.disposables)(e => { + switch (e.key) { + case 'scm.viewMode': + this.viewMode = this.getViewMode(); + break; + case 'scm.viewSortKey': + this.viewSortKey = this.getViewSortKey(); + break; + } + })); + + this._register(this.storageService.onWillSaveState(e => { + this.viewMode = this.getViewMode(); + this.viewSortKey = this.getViewSortKey(); + + this.storeTreeViewState(); + })); this._register(this.instantiationService.createInstance(ScmInputContentProvider)); this._register(Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire())); @@ -2265,28 +1963,76 @@ export class SCMViewPane extends ViewPane { protected override renderBody(container: HTMLElement): void { super.renderBody(container); - // List - this.listContainer = append(container, $('.scm-view.show-file-icons')); - this.listContainer.classList.add('file-icon-themable-tree'); - this.listContainer.classList.add('show-file-icons'); - - const overflowWidgetsDomNode = $('.scm-overflow-widgets-container.monaco-editor'); + // Tree + this.treeContainer = append(container, $('.scm-view.show-file-icons')); + this.treeContainer.classList.add('file-icon-themable-tree'); + this.treeContainer.classList.add('show-file-icons'); - const updateActionsVisibility = () => this.listContainer.classList.toggle('show-actions', this.configurationService.getValue('scm.alwaysShowActions')); + const updateActionsVisibility = () => this.treeContainer.classList.toggle('show-actions', this.configurationService.getValue('scm.alwaysShowActions')); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'), this.disposables)(updateActionsVisibility)); updateActionsVisibility(); const updateProviderCountVisibility = () => { const value = this.configurationService.getValue<'hidden' | 'auto' | 'visible'>('scm.providerCountBadge'); - this.listContainer.classList.toggle('hide-provider-counts', value === 'hidden'); - this.listContainer.classList.toggle('auto-provider-counts', value === 'auto'); + this.treeContainer.classList.toggle('hide-provider-counts', value === 'hidden'); + this.treeContainer.classList.toggle('auto-provider-counts', value === 'auto'); }; this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'), this.disposables)(updateProviderCountVisibility)); updateProviderCountVisibility(); - this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, overflowWidgetsDomNode, (input, height) => { this.tree.updateElementHeight(input, height); }); - const delegate = new ListDelegate(this.inputRenderer); + this.createTree(this.treeContainer); + + this._register(this.onDidChangeBodyVisibility(async visible => { + if (visible) { + await this.tree.setInput(this.scmViewService, this.loadTreeViewState()); + + const updateActionButtonVisibility = () => { + this._showActionButton = this.configurationService.getValue('scm.showActionButton'); + this.updateChildren(); + }; + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.showActionButton'), this.visibilityDisposables)(updateActionButtonVisibility, this); + updateActionButtonVisibility(); + + const updateRepositoryVisibility = () => { + this._alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories'); + this.updateChildren(); + this.updateActions(); + }; + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.visibilityDisposables)(updateRepositoryVisibility, this); + updateRepositoryVisibility(); + + // Add visible repositories + this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.visibilityDisposables); + this.onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); + + // Select resource for active editor + this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); + this.onDidActiveEditorChange(); + + // Restore scroll position + if (typeof this.treeScrollTop === 'number') { + this.tree.scrollTop = this.treeScrollTop; + this.treeScrollTop = undefined; + } + } else { + this.visibilityDisposables.clear(); + this.onDidChangeVisibleRepositories({ added: Iterable.empty(), removed: [...this.items.keys()] }); + this.treeScrollTop = this.tree.scrollTop; + } + + this.updateRepositoryContextKeys(); + this.updateRepositoryCollapseAllContextKeys(); + })); + + this._register(this.instantiationService.createInstance(RepositoryVisibilityActionController)); + this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); + this.updateIndentStyles(this.themeService.getFileIconTheme()); + } + + private createTree(container: HTMLElement): void { + const overflowWidgetsDomNode = $('.scm-overflow-widgets-container.monaco-editor'); + this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, overflowWidgetsDomNode, (input, height) => { this.tree.updateElementHeight(input, height); }); this.actionButtonRenderer = this.instantiationService.createInstance(ActionButtonRenderer); this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); @@ -2301,32 +2047,26 @@ export class SCMViewPane extends ViewPane { this.inputRenderer, this.actionButtonRenderer, this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)), - this._register(this.instantiationService.createInstance(ResourceRenderer, () => this._viewModel, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner)) + this._register(this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner)) ]; - const filter = new SCMTreeFilter(); - const sorter = new SCMTreeSorter(() => this._viewModel); - const keyboardNavigationLabelProvider = this.instantiationService.createInstance(SCMTreeKeyboardNavigationLabelProvider, () => this._viewModel); - const identityProvider = new SCMResourceIdentityProvider(); - const dnd = new SCMTreeDragAndDrop(this.instantiationService); - this.tree = this.instantiationService.createInstance( WorkbenchCompressibleAsyncDataTree, 'SCM Tree Repo', - this.listContainer, - delegate, + container, + new ListDelegate(this.inputRenderer), new SCMTreeCompressionDelegate(), renderers, - this.instantiationService.createInstance(SCMTreeDataSource, () => this._viewModel), + this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode, () => this.alwaysShowRepositories, () => this.showActionButton), { - transformOptimization: false, - identityProvider, - dnd, horizontalScrolling: false, setRowLineHeight: false, - filter, - sorter, - keyboardNavigationLabelProvider, + transformOptimization: false, + dnd: new SCMTreeDragAndDrop(this.instantiationService), + filter: new SCMTreeFilter(), + identityProvider: new SCMResourceIdentityProvider(), + sorter: new SCMTreeSorter(() => this.viewMode, () => this.viewSortKey), + keyboardNavigationLabelProvider: this.instantiationService.createInstance(SCMTreeKeyboardNavigationLabelProvider, () => this.viewMode), overrideStyles: { listBackground: this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND }, @@ -2334,38 +2074,21 @@ export class SCMViewPane extends ViewPane { accessibilityProvider: this.instantiationService.createInstance(SCMAccessibilityProvider) }) as WorkbenchCompressibleAsyncDataTree; + this._register(this.tree); this._register(this.tree.onDidOpen(this.open, this)); - this._register(this.tree.onContextMenu(this.onListContextMenu, this)); this._register(this.tree.onDidScroll(this.inputRenderer.clearValidation, this.inputRenderer)); - this._register(this.tree); - - append(this.listContainer, overflowWidgetsDomNode); - - this._register(this.instantiationService.createInstance(RepositoryVisibilityActionController)); - - this._viewModel = this._register(this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer)); - this._viewModel.setInput(); - - this.updateIndentStyles(this.themeService.getFileIconTheme()); - this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); - this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); - - this._register(this.onDidChangeBodyVisibility(this._viewModel.setVisible, this._viewModel)); + Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element), this.disposables) + (this.updateRepositoryCollapseAllContextKeys, this, this.disposables); - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.disposables)(this.updateActions, this)); - this.updateActions(); + append(container, overflowWidgetsDomNode); } private updateIndentStyles(theme: IFileIconTheme): void { - this.listContainer.classList.toggle('list-view-mode', this._viewModel.mode === ViewModelMode.List); - this.listContainer.classList.toggle('tree-view-mode', this._viewModel.mode === ViewModelMode.Tree); - this.listContainer.classList.toggle('align-icons-and-twisties', (this._viewModel.mode === ViewModelMode.List && theme.hasFileIcons) || (theme.hasFileIcons && !theme.hasFolderIcons)); - this.listContainer.classList.toggle('hide-arrows', this._viewModel.mode === ViewModelMode.Tree && theme.hidesExplorerArrows === true); - } - - private onDidChangeMode(): void { - this.updateIndentStyles(this.themeService.getFileIconTheme()); + this.treeContainer.classList.toggle('list-view-mode', this.viewMode === ViewMode.List); + this.treeContainer.classList.toggle('tree-view-mode', this.viewMode === ViewMode.Tree); + this.treeContainer.classList.toggle('align-icons-and-twisties', (this.viewMode === ViewMode.List && theme.hasFileIcons) || (theme.hasFileIcons && !theme.hasFolderIcons)); + this.treeContainer.classList.toggle('hide-arrows', this.viewMode === ViewMode.Tree && theme.hidesExplorerArrows === true); } protected override layoutBody(height: number | undefined = this.layoutCache.height, width: number | undefined = this.layoutCache.width): void { @@ -2381,18 +2104,10 @@ export class SCMViewPane extends ViewPane { this.layoutCache.width = width; this._onDidLayout.fire(); - this.listContainer.style.height = `${height}px`; + this.treeContainer.style.height = `${height}px`; this.tree.layout(height, width); } - override focus(): void { - super.focus(); - - if (this.isExpanded()) { - this._viewModel.focus(); - } - } - private async open(e: IOpenEvent): Promise { if (!e.element) { return; @@ -2461,6 +2176,94 @@ export class SCMViewPane extends ViewPane { } } + private onDidActiveEditorChange(): void { + if (!this.configurationService.getValue('scm.autoReveal')) { + return; + } + + const uri = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); + + if (!uri) { + return; + } + + this.asyncOperationSequencer.queue(async () => { + for (const repository of this.scmViewService.visibleRepositories) { + const item = this.items.get(repository); + + if (!item) { + continue; + } + + // go backwards from last group + for (let j = repository.provider.groups.length - 1; j >= 0; j--) { + const groupItem = repository.provider.groups[j]; + const resource = this.viewMode === ViewMode.Tree + ? groupItem.resourceTree.getNode(uri)?.element + : groupItem.resources.find(r => this.uriIdentityService.extUri.isEqual(r.sourceUri, uri)); + + if (resource) { + this.tree.reveal(resource); + this.tree.setSelection([resource]); + this.tree.setFocus([resource]); + return; + } + } + } + }); + } + + private onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void { + // Added repositories + for (const repository of added) { + const repositoryDisposables = new DisposableStore(); + + repositoryDisposables.add(repository.provider.onDidChange(() => this.updateChildren())); + repositoryDisposables.add(repository.input.onDidChangeActionButton(() => this.updateChildren())); + repositoryDisposables.add(repository.input.onDidChangeVisibility(() => this.updateChildren())); + repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(() => { + this.updateChildren(); + this.onDidActiveEditorChange(); + })); + + const resourceGroupDisposables = repositoryDisposables.add(new DisposableMap()); + + const onDidChangeResourceGroups = () => { + for (const [resourceGroup] of resourceGroupDisposables) { + if (!repository.provider.groups.includes(resourceGroup)) { + resourceGroupDisposables.deleteAndDispose(resourceGroup); + } + } + + for (const resourceGroup of repository.provider.groups) { + if (!resourceGroupDisposables.has(resourceGroup)) { + const disposableStore = new DisposableStore(); + + disposableStore.add(resourceGroup.onDidChange(() => this.updateChildren())); + disposableStore.add(resourceGroup.onDidChangeResources(() => { + this.updateChildren(); + this.onDidActiveEditorChange(); + })); + resourceGroupDisposables.set(resourceGroup, disposableStore); + } + } + }; + + repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(onDidChangeResourceGroups)); + onDidChangeResourceGroups(); + + this.items.set(repository, repositoryDisposables); + } + + // Removed repositories + for (const repository of removed) { + this.items.deleteAndDispose(repository); + } + + this.updateChildren(); + this.onDidActiveEditorChange(); + } + private onListContextMenu(e: ITreeContextMenuEvent): void { if (!e.element) { const menu = this.menuService.createMenu(Menus.ViewSort, this.contextKeyService); @@ -2523,6 +2326,118 @@ export class SCMViewPane extends ViewPane { .filter(r => !!r && !isSCMResourceGroup(r))! as any; } + private getViewMode(): ViewMode { + let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewMode.List : ViewMode.Tree; + const storageMode = this.storageService.get(`scm.viewMode`, StorageScope.WORKSPACE) as ViewMode; + if (typeof storageMode === 'string') { + mode = storageMode; + } + + return mode; + } + + private getViewSortKey(): ViewSortKey { + // Tree + if (this._viewMode === ViewMode.Tree) { + return ViewSortKey.Path; + } + + // List + let viewSortKey: ViewSortKey; + const viewSortKeyString = this.configurationService.getValue<'path' | 'name' | 'status'>('scm.defaultViewSortKey'); + switch (viewSortKeyString) { + case 'name': + viewSortKey = ViewSortKey.Name; + break; + case 'status': + viewSortKey = ViewSortKey.Status; + break; + default: + viewSortKey = ViewSortKey.Path; + break; + } + + const storageSortKey = this.storageService.get(`scm.viewSortKey`, StorageScope.WORKSPACE) as ViewSortKey; + if (typeof storageSortKey === 'string') { + viewSortKey = storageSortKey; + } + + return viewSortKey; + } + + private loadTreeViewState(): IAsyncDataTreeViewState | undefined { + const storageViewState = this.storageService.get('scm.viewState2', StorageScope.WORKSPACE); + if (!storageViewState) { + return undefined; + } + + try { + const treeViewState = JSON.parse(storageViewState); + return treeViewState; + } catch { + return undefined; + } + } + + private storeTreeViewState() { + this.storageService.store('scm.viewState2', JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + + private updateChildren(element?: ISCMRepository | ISCMResourceGroup, recursive?: boolean, rerender?: boolean) { + this.asyncOperationSequencer.queue(async () => { + this.updateRepositoryContextKeys(); + const focusedInput = this.inputRenderer.getFocusedInput(); + + await this.tree.updateChildren(element, recursive, rerender); + + if (focusedInput) { + this.inputRenderer.getRenderedInputWidget(focusedInput)?.focus(); + } + + this.updateRepositoryCollapseAllContextKeys(); + }); + } + + private updateRepositoryContextKeys(): void { + if (!this.alwaysShowRepositories && this.items.size === 1) { + const provider = Iterable.first(this.items.keys())!.provider; + this.scmProviderContextKey.set(provider.contextValue); + this.scmProviderRootUriContextKey.set(provider.rootUri?.toString()); + this.scmProviderHasRootUriContextKey.set(!!provider.rootUri); + } else { + this.scmProviderContextKey.set(undefined); + this.scmProviderRootUriContextKey.set(undefined); + this.scmProviderHasRootUriContextKey.set(false); + } + } + + private updateRepositoryCollapseAllContextKeys(): void { + if (!this.isBodyVisible() || this.scmViewService.visibleRepositories.length === 1) { + this.isAnyRepositoryCollapsibleContextKey.set(false); + this.areAllRepositoriesCollapsedContextKey.set(false); + return; + } + + this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r))); + this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); + } + + collapseAllRepositories(): void { + for (const repository of this.scmViewService.visibleRepositories) { + if (this.tree.isCollapsible(repository)) { + this.tree.collapse(repository); + } + } + } + + expandAllRepositories(): void { + for (const repository of this.scmViewService.visibleRepositories) { + if (this.tree.isCollapsible(repository)) { + this.tree.expand(repository); + } + } + } + override shouldShowWelcome(): boolean { return this.scmService.repositoryCount === 0; } @@ -2531,8 +2446,29 @@ export class SCMViewPane extends ViewPane { return this.scmViewService.visibleRepositories.length === 1 ? this.scmViewService.visibleRepositories[0].provider : undefined; } + override focus(): void { + super.focus(); + + if (this.isExpanded()) { + if (this.tree.getFocus().length === 0) { + for (const repository of this.scmViewService.visibleRepositories) { + const widget = this.inputRenderer.getRenderedInputWidget(repository.input); + + if (widget) { + widget.focus(); + return; + } + } + } + + this.tree.domFocus(); + } + } + override dispose(): void { + this.visibilityDisposables.dispose(); this.disposables.dispose(); + this.items.dispose(); super.dispose(); } } @@ -2540,7 +2476,9 @@ export class SCMViewPane extends ViewPane { class SCMTreeDataSource implements IAsyncDataSource { constructor( - private readonly viewModelProvider: () => ViewModel, + private readonly viewMode: () => ViewMode, + private readonly alwaysShowRepositories: () => boolean, + private readonly showActionButton: () => boolean, @ISCMViewService private readonly scmViewService: ISCMViewService) { } hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean { @@ -2565,7 +2503,7 @@ class SCMTreeDataSource implements IAsyncDataSource | Promise> { const repositoryCount = this.scmViewService.visibleRepositories.length; - const alwaysShowRepositories = this.viewModelProvider().alwaysShowRepositories; + const alwaysShowRepositories = this.alwaysShowRepositories(); if (isSCMViewService(inputOrElement) && (repositoryCount > 1 || alwaysShowRepositories)) { return this.scmViewService.visibleRepositories; @@ -2575,7 +2513,7 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Thu, 2 Nov 2023 15:25:05 +0100 Subject: [PATCH 13/26] Move some of the context keys into RepositoryVisibilityActionController --- .../contrib/scm/browser/scmViewPane.ts | 120 ++++++++++-------- 1 file changed, 64 insertions(+), 56 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 30abb19abcda9..99f72c2d451a3 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -25,7 +25,7 @@ import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService } from './util'; import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, ThrottledDelayer, Sequencer } from 'vs/base/common/async'; import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { ResourceTree, IResourceNode } from 'vs/base/common/resourceTree'; @@ -1001,18 +1001,29 @@ interface RepositoryVisibilityItem { class RepositoryVisibilityActionController { + private alwaysShowRepositories = false; private items = new Map(); private repositoryCountContextKey: IContextKey; private repositoryVisibilityCountContextKey: IContextKey; + private scmProviderContextKey: IContextKey; + private scmProviderRootUriContextKey: IContextKey; + private scmProviderHasRootUriContextKey: IContextKey; private readonly disposables = new DisposableStore(); constructor( - @ISCMViewService private scmViewService: ISCMViewService, - @ISCMService scmService: ISCMService, - @IContextKeyService private contextKeyService: IContextKeyService + @IContextKeyService private contextKeyService: IContextKeyService, + @ISCMViewService private readonly scmViewService: ISCMViewService, + @IConfigurationService configurationService: IConfigurationService, + @ISCMService scmService: ISCMService ) { this.repositoryCountContextKey = ContextKeys.RepositoryCount.bindTo(contextKeyService); this.repositoryVisibilityCountContextKey = ContextKeys.RepositoryVisibilityCount.bindTo(contextKeyService); + this.scmProviderContextKey = ContextKeys.SCMProvider.bindTo(contextKeyService); + this.scmProviderRootUriContextKey = ContextKeys.SCMProviderRootUri.bindTo(contextKeyService); + this.scmProviderHasRootUriContextKey = ContextKeys.SCMProviderHasRootUri.bindTo(contextKeyService); + + configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); + this.onDidChangeConfiguration(); scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.disposables); scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); @@ -1041,13 +1052,13 @@ class RepositoryVisibilityActionController { } }); - this.updateRepositoriesCounts(); + this.updateRepositoryContextKeys(); } private onDidRemoveRepository(repository: ISCMRepository): void { this.items.get(repository)?.dispose(); this.items.delete(repository); - this.updateRepositoriesCounts(); + this.updateRepositoryContextKeys(); } private onDidChangeVisibleRepositories(): void { @@ -1066,9 +1077,27 @@ class RepositoryVisibilityActionController { this.repositoryVisibilityCountContextKey.set(count); } - private updateRepositoriesCounts(): void { + private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { + if (!e || e.affectsConfiguration('scm.alwaysShowRepositories')) { + this.alwaysShowRepositories = this.contextKeyService.getContextKeyValue('scm.alwaysShowRepositories') === true; + this.updateRepositoryContextKeys(); + } + } + + private updateRepositoryContextKeys(): void { this.repositoryCountContextKey.set(this.items.size); this.repositoryVisibilityCountContextKey.set(Iterable.reduce(this.items.keys(), (r, repository) => r + (this.scmViewService.isVisible(repository) ? 1 : 0), 0)); + + if (!this.alwaysShowRepositories && this.items.size === 1) { + const provider = Iterable.first(this.items.keys())!.provider; + this.scmProviderContextKey.set(provider.contextValue); + this.scmProviderRootUriContextKey.set(provider.rootUri?.toString()); + this.scmProviderHasRootUriContextKey.set(!!provider.rootUri); + } else { + this.scmProviderContextKey.set(undefined); + this.scmProviderRootUriContextKey.set(undefined); + this.scmProviderHasRootUriContextKey.set(false); + } } dispose(): void { @@ -1893,29 +1922,26 @@ export class SCMViewPane extends ViewPane { private viewSortKeyContextKey: IContextKey; private areAllRepositoriesCollapsedContextKey: IContextKey; private isAnyRepositoryCollapsibleContextKey: IContextKey; - private scmProviderContextKey: IContextKey; - private scmProviderRootUriContextKey: IContextKey; - private scmProviderHasRootUriContextKey: IContextKey; private readonly disposables = new DisposableStore(); constructor( options: IViewPaneOptions, + @ICommandService private readonly commandService: ICommandService, + @IEditorService private readonly editorService: IEditorService, + @IMenuService private readonly menuService: IMenuService, @ISCMService private readonly scmService: ISCMService, @ISCMViewService private readonly scmViewService: ISCMViewService, + @IStorageService private readonly storageService: IStorageService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IKeybindingService keybindingService: IKeybindingService, @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, - @ICommandService private readonly commandService: ICommandService, - @IEditorService private readonly editorService: IEditorService, @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, - @IMenuService private readonly menuService: IMenuService, - @IStorageService private readonly storageService: IStorageService, @IOpenerService openerService: IOpenerService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ITelemetryService telemetryService: ITelemetryService, ) { super({ ...options, titleMenuId: MenuId.SCMTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); @@ -1931,9 +1957,6 @@ export class SCMViewPane extends ViewPane { this.viewSortKeyContextKey.set(this.viewSortKey); this.areAllRepositoriesCollapsedContextKey = ContextKeys.SCMViewAreAllRepositoriesCollapsed.bindTo(contextKeyService); this.isAnyRepositoryCollapsibleContextKey = ContextKeys.SCMViewIsAnyRepositoryCollapsible.bindTo(contextKeyService); - this.scmProviderContextKey = ContextKeys.SCMProvider.bindTo(contextKeyService); - this.scmProviderRootUriContextKey = ContextKeys.SCMProviderRootUri.bindTo(contextKeyService); - this.scmProviderHasRootUriContextKey = ContextKeys.SCMProviderHasRootUri.bindTo(contextKeyService); this._onDidLayout = new Emitter(); this.layoutCache = { height: undefined, width: undefined, onDidChange: this._onDidLayout.event }; @@ -1960,6 +1983,23 @@ export class SCMViewPane extends ViewPane { this._register(Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire())); } + protected override layoutBody(height: number | undefined = this.layoutCache.height, width: number | undefined = this.layoutCache.width): void { + if (height === undefined) { + return; + } + + if (width !== undefined) { + super.layoutBody(height, width); + } + + this.layoutCache.height = height; + this.layoutCache.width = width; + this._onDidLayout.fire(); + + this.treeContainer.style.height = `${height}px`; + this.tree.layout(height, width); + } + protected override renderBody(container: HTMLElement): void { super.renderBody(container); @@ -2020,7 +2060,6 @@ export class SCMViewPane extends ViewPane { this.treeScrollTop = this.tree.scrollTop; } - this.updateRepositoryContextKeys(); this.updateRepositoryCollapseAllContextKeys(); })); @@ -2084,30 +2123,6 @@ export class SCMViewPane extends ViewPane { append(container, overflowWidgetsDomNode); } - private updateIndentStyles(theme: IFileIconTheme): void { - this.treeContainer.classList.toggle('list-view-mode', this.viewMode === ViewMode.List); - this.treeContainer.classList.toggle('tree-view-mode', this.viewMode === ViewMode.Tree); - this.treeContainer.classList.toggle('align-icons-and-twisties', (this.viewMode === ViewMode.List && theme.hasFileIcons) || (theme.hasFileIcons && !theme.hasFolderIcons)); - this.treeContainer.classList.toggle('hide-arrows', this.viewMode === ViewMode.Tree && theme.hidesExplorerArrows === true); - } - - protected override layoutBody(height: number | undefined = this.layoutCache.height, width: number | undefined = this.layoutCache.width): void { - if (height === undefined) { - return; - } - - if (width !== undefined) { - super.layoutBody(height, width); - } - - this.layoutCache.height = height; - this.layoutCache.width = width; - this._onDidLayout.fire(); - - this.treeContainer.style.height = `${height}px`; - this.tree.layout(height, width); - } - private async open(e: IOpenEvent): Promise { if (!e.element) { return; @@ -2385,7 +2400,6 @@ export class SCMViewPane extends ViewPane { private updateChildren(element?: ISCMRepository | ISCMResourceGroup, recursive?: boolean, rerender?: boolean) { this.asyncOperationSequencer.queue(async () => { - this.updateRepositoryContextKeys(); const focusedInput = this.inputRenderer.getFocusedInput(); await this.tree.updateChildren(element, recursive, rerender); @@ -2398,21 +2412,15 @@ export class SCMViewPane extends ViewPane { }); } - private updateRepositoryContextKeys(): void { - if (!this.alwaysShowRepositories && this.items.size === 1) { - const provider = Iterable.first(this.items.keys())!.provider; - this.scmProviderContextKey.set(provider.contextValue); - this.scmProviderRootUriContextKey.set(provider.rootUri?.toString()); - this.scmProviderHasRootUriContextKey.set(!!provider.rootUri); - } else { - this.scmProviderContextKey.set(undefined); - this.scmProviderRootUriContextKey.set(undefined); - this.scmProviderHasRootUriContextKey.set(false); - } + private updateIndentStyles(theme: IFileIconTheme): void { + this.treeContainer.classList.toggle('list-view-mode', this.viewMode === ViewMode.List); + this.treeContainer.classList.toggle('tree-view-mode', this.viewMode === ViewMode.Tree); + this.treeContainer.classList.toggle('align-icons-and-twisties', (this.viewMode === ViewMode.List && theme.hasFileIcons) || (theme.hasFileIcons && !theme.hasFolderIcons)); + this.treeContainer.classList.toggle('hide-arrows', this.viewMode === ViewMode.Tree && theme.hidesExplorerArrows === true); } private updateRepositoryCollapseAllContextKeys(): void { - if (!this.isBodyVisible() || this.scmViewService.visibleRepositories.length === 1) { + if (!this.isBodyVisible() || this.items.size === 1) { this.isAnyRepositoryCollapsibleContextKey.set(false); this.areAllRepositoriesCollapsedContextKey.set(false); return; From 459d844d12f2ef3753b02f572c3288da3b0b4c22 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Thu, 2 Nov 2023 15:40:30 +0100 Subject: [PATCH 14/26] Remove the this._register calls --- .../contrib/scm/browser/scmViewPane.ts | 65 +++++++++---------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 99f72c2d451a3..34c8c44be92c1 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1961,7 +1961,7 @@ export class SCMViewPane extends ViewPane { this._onDidLayout = new Emitter(); this.layoutCache = { height: undefined, width: undefined, onDidChange: this._onDidLayout.event }; - this._register(this.storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, this.disposables)(e => { + this.storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, this.disposables)(e => { switch (e.key) { case 'scm.viewMode': this.viewMode = this.getViewMode(); @@ -1970,17 +1970,17 @@ export class SCMViewPane extends ViewPane { this.viewSortKey = this.getViewSortKey(); break; } - })); + }, this, this.disposables); - this._register(this.storageService.onWillSaveState(e => { + this.storageService.onWillSaveState(e => { this.viewMode = this.getViewMode(); this.viewSortKey = this.getViewSortKey(); this.storeTreeViewState(); - })); + }, this, this.disposables); - this._register(this.instantiationService.createInstance(ScmInputContentProvider)); - this._register(Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire())); + this.disposables.add(this.instantiationService.createInstance(ScmInputContentProvider)); + Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire(), this, this.disposables); } protected override layoutBody(height: number | undefined = this.layoutCache.height, width: number | undefined = this.layoutCache.width): void { @@ -2009,7 +2009,7 @@ export class SCMViewPane extends ViewPane { this.treeContainer.classList.add('show-file-icons'); const updateActionsVisibility = () => this.treeContainer.classList.toggle('show-actions', this.configurationService.getValue('scm.alwaysShowActions')); - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'), this.disposables)(updateActionsVisibility)); + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'), this.disposables)(updateActionsVisibility, this, this.disposables); updateActionsVisibility(); const updateProviderCountVisibility = () => { @@ -2017,12 +2017,12 @@ export class SCMViewPane extends ViewPane { this.treeContainer.classList.toggle('hide-provider-counts', value === 'hidden'); this.treeContainer.classList.toggle('auto-provider-counts', value === 'auto'); }; - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'), this.disposables)(updateProviderCountVisibility)); + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'), this.disposables)(updateProviderCountVisibility, this, this.disposables); updateProviderCountVisibility(); this.createTree(this.treeContainer); - this._register(this.onDidChangeBodyVisibility(async visible => { + this.onDidChangeBodyVisibility(async visible => { if (visible) { await this.tree.setInput(this.scmViewService, this.loadTreeViewState()); @@ -2030,7 +2030,7 @@ export class SCMViewPane extends ViewPane { this._showActionButton = this.configurationService.getValue('scm.showActionButton'); this.updateChildren(); }; - Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.showActionButton'), this.visibilityDisposables)(updateActionButtonVisibility, this); + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.showActionButton'), this.visibilityDisposables)(updateActionButtonVisibility, this, this.visibilityDisposables); updateActionButtonVisibility(); const updateRepositoryVisibility = () => { @@ -2038,7 +2038,7 @@ export class SCMViewPane extends ViewPane { this.updateChildren(); this.updateActions(); }; - Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.visibilityDisposables)(updateRepositoryVisibility, this); + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.visibilityDisposables)(updateRepositoryVisibility, this, this.visibilityDisposables); updateRepositoryVisibility(); // Add visible repositories @@ -2061,10 +2061,11 @@ export class SCMViewPane extends ViewPane { } this.updateRepositoryCollapseAllContextKeys(); - })); + }, this, this.disposables); + + this.disposables.add(this.instantiationService.createInstance(RepositoryVisibilityActionController)); - this._register(this.instantiationService.createInstance(RepositoryVisibilityActionController)); - this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); + this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this, this.disposables); this.updateIndentStyles(this.themeService.getFileIconTheme()); } @@ -2075,19 +2076,11 @@ export class SCMViewPane extends ViewPane { this.actionButtonRenderer = this.instantiationService.createInstance(ActionButtonRenderer); this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); - this._register(this.listLabels); + this.disposables.add(this.listLabels); const actionRunner = new RepositoryPaneActionRunner(() => this.getSelectedResources()); - this._register(actionRunner); - this._register(actionRunner.onWillRun(() => this.tree.domFocus())); - - const renderers: ICompressibleTreeRenderer[] = [ - this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)), - this.inputRenderer, - this.actionButtonRenderer, - this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)), - this._register(this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner)) - ]; + actionRunner.onWillRun(() => this.tree.domFocus(), this, this.disposables); + this.disposables.add(actionRunner); this.tree = this.instantiationService.createInstance( WorkbenchCompressibleAsyncDataTree, @@ -2095,14 +2088,20 @@ export class SCMViewPane extends ViewPane { container, new ListDelegate(this.inputRenderer), new SCMTreeCompressionDelegate(), - renderers, + [ + this.inputRenderer, + this.actionButtonRenderer, + this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)), + this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)), + this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner) + ], this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode, () => this.alwaysShowRepositories, () => this.showActionButton), { horizontalScrolling: false, setRowLineHeight: false, transformOptimization: false, - dnd: new SCMTreeDragAndDrop(this.instantiationService), filter: new SCMTreeFilter(), + dnd: new SCMTreeDragAndDrop(this.instantiationService), identityProvider: new SCMResourceIdentityProvider(), sorter: new SCMTreeSorter(() => this.viewMode, () => this.viewSortKey), keyboardNavigationLabelProvider: this.instantiationService.createInstance(SCMTreeKeyboardNavigationLabelProvider, () => this.viewMode), @@ -2113,12 +2112,12 @@ export class SCMViewPane extends ViewPane { accessibilityProvider: this.instantiationService.createInstance(SCMAccessibilityProvider) }) as WorkbenchCompressibleAsyncDataTree; - this._register(this.tree); - this._register(this.tree.onDidOpen(this.open, this)); - this._register(this.tree.onContextMenu(this.onListContextMenu, this)); - this._register(this.tree.onDidScroll(this.inputRenderer.clearValidation, this.inputRenderer)); - Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element), this.disposables) - (this.updateRepositoryCollapseAllContextKeys, this, this.disposables); + this.disposables.add(this.tree); + + this.tree.onDidOpen(this.open, this, this.disposables); + this.tree.onContextMenu(this.onListContextMenu, this, this.disposables); + this.tree.onDidScroll(this.inputRenderer.clearValidation, this.inputRenderer, this.disposables); + Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element), this.disposables)(this.updateRepositoryCollapseAllContextKeys, this, this.disposables); append(container, overflowWidgetsDomNode); } From f95ff13fe2c0fbaa488b1aba5bd8b2bb37ef878b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Fri, 3 Nov 2023 16:11:49 +0100 Subject: [PATCH 15/26] AsyncDataTree.expandTo --- src/vs/base/browser/ui/tree/abstractTree.ts | 4 +++ src/vs/base/browser/ui/tree/asyncDataTree.ts | 22 +++++++++++++++ src/vs/base/browser/ui/tree/tree.ts | 1 + .../contrib/scm/browser/scmViewPane.ts | 28 ++++++++++++++++++- 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index cf8b2a58caa95..4377e0dcde91a 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1860,6 +1860,10 @@ export abstract class AbstractTree implements IDisposable return this.model.isCollapsed(location); } + expandTo(location: TRef): void { + this.model.expandTo(location); + } + triggerTypeNavigation(): void { this.view.triggerTypeNavigation(); } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index be95156a79b0e..2ccd22354d3f3 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -645,6 +645,28 @@ export class AsyncDataTree implements IDisposable this.tree.expandAll(); } + async expandTo(element: T): Promise { + if (!this.dataSource.getParent) { + throw new Error('Can\'t expand to element without getParent method'); + } + + const elements: T[] = []; + + while (!this.hasNode(element)) { + element = this.dataSource.getParent(element) as T; + + if (element !== this.root.element) { + elements.push(element); + } + } + + for (const element of Iterable.reverse(elements)) { + await this.expand(element); + } + + this.tree.expandTo(this.getDataNode(element)); + } + collapseAll(): void { this.tree.collapseAll(); } diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 262badedf9fd2..e929dc7de091b 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -196,6 +196,7 @@ export interface IDataSource { export interface IAsyncDataSource { hasChildren(element: TInput | T): boolean; getChildren(element: TInput | T): Iterable | Promise>; + getParent?(element: T): TInput | T; } export const enum TreeDragOverBubble { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 34c8c44be92c1..c0465e6c7a907 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2217,7 +2217,7 @@ export class SCMViewPane extends ViewPane { : groupItem.resources.find(r => this.uriIdentityService.extUri.isEqual(r.sourceUri, uri)); if (resource) { - this.tree.reveal(resource); + await this.tree.expandTo(resource); this.tree.setSelection([resource]); this.tree.setFocus([resource]); return; @@ -2569,6 +2569,32 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Fri, 3 Nov 2023 16:40:50 +0100 Subject: [PATCH 16/26] Fix expand all/collapse all buttons --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index c0465e6c7a907..b52d25709ba6c 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2117,7 +2117,7 @@ export class SCMViewPane extends ViewPane { this.tree.onDidOpen(this.open, this, this.disposables); this.tree.onContextMenu(this.onListContextMenu, this, this.disposables); this.tree.onDidScroll(this.inputRenderer.clearValidation, this.inputRenderer, this.disposables); - Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element), this.disposables)(this.updateRepositoryCollapseAllContextKeys, this, this.disposables); + Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element?.element), this.disposables)(this.updateRepositoryCollapseAllContextKeys, this, this.disposables); append(container, overflowWidgetsDomNode); } From 74e8a1a67a4006da01e4b7f2f98da92f520a7f1c Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:34:03 +0100 Subject: [PATCH 17/26] Initial implementation of incoming/outgoing changes --- .../contrib/scm/browser/scmSyncViewPane.ts | 6 +- .../contrib/scm/browser/scmViewPane.ts | 498 ++++++++++++++++-- src/vs/workbench/contrib/scm/browser/util.ts | 35 ++ 3 files changed, 478 insertions(+), 61 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts index b449ea6d28a26..f21e8f03df0d7 100644 --- a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts @@ -117,7 +117,7 @@ const ContextKeys = { ViewMode: new RawContextKey('scmSyncViewMode', ViewMode.List), }; -interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup { +export interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup { readonly description?: string; readonly ancestor?: string; readonly count?: number; @@ -125,12 +125,12 @@ interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup { readonly type: 'historyItemGroup'; } -interface SCMHistoryItemTreeElement extends ISCMHistoryItem { +export interface SCMHistoryItemTreeElement extends ISCMHistoryItem { readonly historyItemGroup: SCMHistoryItemGroupTreeElement; readonly type: 'historyItem'; } -interface SCMHistoryItemChangeTreeElement extends ISCMHistoryItemChange { +export interface SCMHistoryItemChangeTreeElement extends ISCMHistoryItemChange { readonly historyItem: SCMHistoryItemTreeElement; readonly type: 'historyItemChange'; } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 34c8c44be92c1..cc69ee8a723fa 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { basename, dirname } from 'vs/base/common/resources'; import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, IReference, DisposableMap } from 'vs/base/common/lifecycle'; import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; -import { append, $, Dimension, asCSSUrl, trackFocus, clearNode } from 'vs/base/browser/dom'; +import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; @@ -23,7 +23,7 @@ import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, import { IAction, ActionRunner, Action, Separator } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService } from './util'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode } from './util'; import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, ThrottledDelayer, Sequencer } from 'vs/base/common/async'; @@ -96,8 +96,23 @@ import { FormatOnType } from 'vs/editor/contrib/format/browser/formatActions'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IAsyncDataTreeViewState, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; - -type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | IResourceNode | ISCMResource; +import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane'; +import { stripIcons } from 'vs/base/common/iconLabels'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; + +// type SCMResourceTreeNode = IResourceNode; +// type SCMHistoryItemChangeResourceTreeNode = IResourceNode; +type TreeElement = + ISCMRepository | + ISCMInput | + ISCMActionButton | + ISCMResourceGroup | + ISCMResource | + IResourceNode | + SCMHistoryItemGroupTreeElement | + SCMHistoryItemTreeElement | + SCMHistoryItemChangeTreeElement | + IResourceNode; interface ISCMLayout { height: number | undefined; @@ -677,6 +692,158 @@ class ResourceRenderer implements ICompressibleTreeRenderer { + + static readonly TEMPLATE_ID = 'history-item-group'; + get templateId(): string { return HistoryItemGroupRenderer.TEMPLATE_ID; } + + renderTemplate(container: HTMLElement) { + // hack + (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-twistie'); + + const element = append(container, $('.history-item-group')); + const label = new IconLabel(element, { supportIcons: true }); + const countContainer = append(element, $('.count')); + const count = new CountBadge(countContainer, {}, defaultCountBadgeStyles); + + return { label, count, disposables: new DisposableStore() }; + } + + renderElement(node: ITreeNode, index: number, templateData: HistoryItemGroupTemplate, height: number | undefined): void { + const historyItemGroup = node.element; + templateData.label.setLabel(historyItemGroup.label, historyItemGroup.description); + templateData.count.setCount(historyItemGroup.count ?? 0); + } + + renderCompressedElements(node: ITreeNode, void>, index: number, templateData: HistoryItemGroupTemplate, height: number | undefined): void { + throw new Error('Should never happen since node is incompressible'); + } + disposeTemplate(templateData: HistoryItemGroupTemplate): void { + templateData.disposables.dispose(); + } +} + +interface HistoryItemTemplate { + readonly iconContainer: HTMLElement; + // readonly avatarImg: HTMLImageElement; + readonly iconLabel: IconLabel; + // readonly timestampContainer: HTMLElement; + // readonly timestamp: HTMLSpanElement; + readonly disposables: IDisposable; +} + +class HistoryItemRenderer implements ICompressibleTreeRenderer { + + static readonly TEMPLATE_ID = 'history-item'; + get templateId(): string { return HistoryItemRenderer.TEMPLATE_ID; } + + renderTemplate(container: HTMLElement): HistoryItemTemplate { + // hack + (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-twistie'); + + const element = append(container, $('.history-item')); + const iconLabel = new IconLabel(element, { supportIcons: true }); + + const iconContainer = prepend(iconLabel.element, $('.icon-container')); + // const avatarImg = append(iconContainer, $('img.avatar')) as HTMLImageElement; + + // const timestampContainer = append(iconLabel.element, $('.timestamp-container')); + // const timestamp = append(timestampContainer, $('span.timestamp')); + + return { iconContainer, iconLabel, disposables: new DisposableStore() }; + } + + renderElement(node: ITreeNode, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + const historyItem = node.element; + + templateData.iconContainer.className = 'icon-container'; + if (historyItem.icon && ThemeIcon.isThemeIcon(historyItem.icon)) { + templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(historyItem.icon)); + } + + // if (commit.authorAvatar) { + // templateData.avatarImg.src = commit.authorAvatar; + // templateData.avatarImg.style.display = 'block'; + // templateData.iconContainer.classList.remove(...ThemeIcon.asClassNameArray(Codicon.account)); + // } else { + // templateData.avatarImg.style.display = 'none'; + // templateData.iconContainer.classList.add(...ThemeIcon.asClassNameArray(Codicon.account)); + // } + + templateData.iconLabel.setLabel(historyItem.label, historyItem.description); + + // templateData.timestampContainer.classList.toggle('timestamp-duplicate', commit.hideTimestamp === true); + // templateData.timestamp.textContent = fromNow(commit.timestamp); + } + + renderCompressedElements(node: ITreeNode, void>, index: number, templateData: HistoryItemTemplate, height: number | undefined): void { + throw new Error('Should never happen since node is incompressible'); + } + disposeTemplate(templateData: HistoryItemTemplate): void { + templateData.disposables.dispose(); + } +} + +interface HistoryItemChangeTemplate { + readonly element: HTMLElement; + readonly name: HTMLElement; + readonly fileLabel: IResourceLabel; + readonly decorationIcon: HTMLElement; + readonly disposables: IDisposable; +} + +class HistoryItemChangeRenderer implements ICompressibleTreeRenderer, void, HistoryItemChangeTemplate> { + + static readonly TEMPLATE_ID = 'historyItemChange'; + get templateId(): string { return HistoryItemChangeRenderer.TEMPLATE_ID; } + + constructor( + private readonly viewMode: () => ViewMode, + private readonly labels: ResourceLabels, + @ILabelService private labelService: ILabelService) { } + + renderTemplate(container: HTMLElement): HistoryItemChangeTemplate { + const element = append(container, $('.change')); + const name = append(element, $('.name')); + const fileLabel = this.labels.create(name, { supportDescriptionHighlights: true, supportHighlights: true }); + const decorationIcon = append(element, $('.decoration-icon')); + + return { element, name, fileLabel, decorationIcon, disposables: new DisposableStore() }; + } + + renderElement(node: ITreeNode, void>, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void { + const historyItemChangeOrFolder = node.element; + const uri = ResourceTree.isResourceNode(historyItemChangeOrFolder) ? historyItemChangeOrFolder.element?.uri ?? historyItemChangeOrFolder.uri : historyItemChangeOrFolder.uri; + const fileKind = ResourceTree.isResourceNode(historyItemChangeOrFolder) ? FileKind.FOLDER : FileKind.FILE; + const hidePath = this.viewMode() === ViewMode.Tree; + + templateData.fileLabel.setFile(uri, { fileDecorations: { colors: false, badges: true }, fileKind, hidePath, }); + } + + renderCompressedElements(node: ITreeNode>, void>, index: number, templateData: HistoryItemChangeTemplate, height: number | undefined): void { + const compressed = node.element as ICompressedTreeNode>; + + const folder = compressed.elements[compressed.elements.length - 1]; + const label = compressed.elements.map(e => e.name); + + templateData.fileLabel.setResource({ resource: folder.uri, name: label }, { + fileDecorations: { colors: false, badges: true }, + fileKind: FileKind.FOLDER, + separator: this.labelService.getSeparator(folder.uri.scheme) + }); + } + + disposeTemplate(templateData: HistoryItemChangeTemplate): void { + templateData.disposables.dispose(); + } +} + class ListDelegate implements IListVirtualDelegate { constructor(private readonly inputRenderer: InputRenderer) { } @@ -698,10 +865,18 @@ class ListDelegate implements IListVirtualDelegate { return InputRenderer.TEMPLATE_ID; } else if (isSCMActionButton(element)) { return ActionButtonRenderer.TEMPLATE_ID; - } else if (ResourceTree.isResourceNode(element) || isSCMResource(element)) { + } else if (isSCMResourceGroup(element)) { + return ResourceGroupRenderer.TEMPLATE_ID; + } else if (isSCMResource(element) || isSCMResourceNode(element)) { return ResourceRenderer.TEMPLATE_ID; + } else if (isSCMHistoryItemGroupTreeElement(element)) { + return HistoryItemGroupRenderer.TEMPLATE_ID; + } else if (isSCMHistoryItemTreeElement(element)) { + return HistoryItemRenderer.TEMPLATE_ID; + } else if (isSCMHistoryItemChangeTreeElement(element) || isSCMHistoryItemChangeNode(element)) { + return HistoryItemChangeRenderer.TEMPLATE_ID; } else { - return ResourceGroupRenderer.TEMPLATE_ID; + throw new Error('Unknown element'); } } } @@ -725,6 +900,8 @@ class SCMTreeFilter implements ITreeFilter { return true; } else if (isSCMResourceGroup(element)) { return element.resources.length > 0 || !element.hideWhenEmpty; + } else if (isSCMHistoryItemGroupTreeElement(element)) { + return (element.count ?? 0) > 0; } else { return true; } @@ -766,6 +943,22 @@ export class SCMTreeSorter implements ITreeSorter { return 0; } + if (isSCMHistoryItemGroupTreeElement(one)) { + if (!isSCMHistoryItemGroupTreeElement(other) && !isSCMResourceGroup(other)) { + throw new Error('Invalid comparison'); + } + + return 0; + } + + if (isSCMHistoryItemTreeElement(one)) { + if (!isSCMHistoryItemTreeElement(other)) { + throw new Error('Invalid comparison'); + } + + return 0; + } + // List if (this.viewMode() === ViewMode.List) { // FileName @@ -822,19 +1015,21 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb return undefined; } else if (isSCMResourceGroup(element)) { return element.label; + } else if (isSCMHistoryItemGroupTreeElement(element)) { + return element.label; + } else if (isSCMHistoryItemTreeElement(element)) { + return element.label; } else { if (this.viewMode() === ViewMode.List) { // In List mode match using the file name and the path. // Since we want to match both on the file name and the // full path we return an array of labels. A match in the // file name takes precedence over a match in the path. - const fileName = basename(element.sourceUri); - const filePath = this.labelService.getUriLabel(element.sourceUri, { relative: true }); - - return [fileName, filePath]; + const uri = isSCMResource(element) ? element.sourceUri : element.uri; + return [basename(uri), this.labelService.getUriLabel(uri, { relative: true })]; } else { // In Tree mode only match using the file name - return basename(element.sourceUri); + return basename(isSCMResource(element) ? element.sourceUri : element.uri); } } } @@ -846,10 +1041,7 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb } function getSCMResourceId(element: TreeElement): string { - if (ResourceTree.isResourceNode(element)) { - const group = element.context; - return `folder:${group.provider.id}/${group.id}/$FOLDER/${element.uri.toString()}`; - } else if (isSCMRepository(element)) { + if (isSCMRepository(element)) { const provider = element.provider; return `repo:${provider.id}`; } else if (isSCMInput(element)) { @@ -858,13 +1050,35 @@ function getSCMResourceId(element: TreeElement): string { } else if (isSCMActionButton(element)) { const provider = element.repository.provider; return `actionButton:${provider.id}`; + } else if (isSCMResourceGroup(element)) { + const provider = element.provider; + return `resourceGroup:${provider.id}/${element.id}`; } else if (isSCMResource(element)) { const group = element.resourceGroup; const provider = group.provider; return `resource:${provider.id}/${group.id}/${element.sourceUri.toString()}`; + } else if (isSCMResourceNode(element)) { + const group = element.context; + return `folder:${group.provider.id}/${group.id}/$FOLDER/${element.uri.toString()}`; + } else if (isSCMHistoryItemGroupTreeElement(element)) { + const provider = element.repository.provider; + return `historyItemGroup:${provider.id}/${element.id}`; + } else if (isSCMHistoryItemTreeElement(element)) { + const historyItemGroup = element.historyItemGroup; + const provider = historyItemGroup.repository.provider; + return `historyItem:${provider.id}/${historyItemGroup.id}/${element.id}`; + } else if (isSCMHistoryItemChangeTreeElement(element)) { + const historyItem = element.historyItem; + const historyItemGroup = historyItem.historyItemGroup; + const provider = historyItemGroup.repository.provider; + return `historyItemChange:${provider.id}/${historyItemGroup.id}/${historyItem.id}/${element.uri.toString()}`; + } else if (isSCMHistoryItemChangeNode(element)) { + const historyItem = element.context; + const historyItemGroup = historyItem.historyItemGroup; + const provider = historyItemGroup.repository.provider; + return `folder:${provider.id}/${historyItemGroup.id}/${historyItem.id}/$FOLDER/${element.uri.toString()}`; } else { - const provider = element.provider; - return `group:${provider.id}/${element.id}`; + throw new Error('Invalid tree element'); } } @@ -907,6 +1121,19 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner) + this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner), + this.instantiationService.createInstance(HistoryItemGroupRenderer), + this.instantiationService.createInstance(HistoryItemRenderer), + this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this.viewMode, this.listLabels), ], this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode, () => this.alwaysShowRepositories, () => this.showActionButton), { @@ -2108,7 +2338,7 @@ export class SCMViewPane extends ViewPane { overrideStyles: { listBackground: this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND }, - collapseByDefault: (e) => false, + collapseByDefault: (e: unknown) => isSCMHistoryItemGroupTreeElement(e) || isSCMHistoryItemTreeElement(e) || isSCMHistoryItemChangeTreeElement(e), accessibilityProvider: this.instantiationService.createInstance(SCMAccessibilityProvider) }) as WorkbenchCompressibleAsyncDataTree; @@ -2128,20 +2358,6 @@ export class SCMViewPane extends ViewPane { } else if (isSCMRepository(e.element)) { this.scmViewService.focus(e.element); return; - } else if (isSCMResourceGroup(e.element)) { - const provider = e.element.provider; - const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider); - if (repository) { - this.scmViewService.focus(repository); - } - return; - } else if (ResourceTree.isResourceNode(e.element)) { - const provider = e.element.context.provider; - const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider); - if (repository) { - this.scmViewService.focus(repository); - } - return; } else if (isSCMInput(e.element)) { this.scmViewService.focus(e.element.repository); @@ -2167,26 +2383,59 @@ export class SCMViewPane extends ViewPane { this.tree.setFocus([], e.browserEvent); return; - } + } else if (isSCMResourceGroup(e.element)) { + const provider = e.element.provider; + const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider); + if (repository) { + this.scmViewService.focus(repository); + } + return; + } else if (isSCMResource(e.element)) { + if (e.element.command?.id === API_OPEN_EDITOR_COMMAND_ID || e.element.command?.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) { + await this.commandService.executeCommand(e.element.command.id, ...(e.element.command.arguments || []), e); + } else { + await e.element.open(!!e.editorOptions.preserveFocus); - // ISCMResource - if (e.element.command?.id === API_OPEN_EDITOR_COMMAND_ID || e.element.command?.id === API_OPEN_DIFF_EDITOR_COMMAND_ID) { - await this.commandService.executeCommand(e.element.command.id, ...(e.element.command.arguments || []), e); - } else { - await e.element.open(!!e.editorOptions.preserveFocus); + if (e.editorOptions.pinned) { + const activeEditorPane = this.editorService.activeEditorPane; - if (e.editorOptions.pinned) { - const activeEditorPane = this.editorService.activeEditorPane; + activeEditorPane?.group.pinEditor(activeEditorPane.input); + } + } + + const provider = e.element.resourceGroup.provider; + const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider); - activeEditorPane?.group.pinEditor(activeEditorPane.input); + if (repository) { + this.scmViewService.focus(repository); } - } + } else if (isSCMResourceNode(e.element)) { + const provider = e.element.context.provider; + const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider); + if (repository) { + this.scmViewService.focus(repository); + } + return; + } else if (isSCMHistoryItemGroupTreeElement(e.element)) { + this.scmViewService.focus(e.element.repository); + + // TODO@lszomoru: focus the history item group + return; + } else if (isSCMHistoryItemTreeElement(e.element)) { + this.scmViewService.focus(e.element.historyItemGroup.repository); - const provider = e.element.resourceGroup.provider; - const repository = Iterable.find(this.scmService.repositories, r => r.provider === provider); + // TODO@lszomoru: focus the history item + return; + } else if (isSCMHistoryItemChangeTreeElement(e.element)) { + if (e.element.originalUri && e.element.modifiedUri) { + await this.commandService.executeCommand(API_OPEN_DIFF_EDITOR_COMMAND_ID, ...toDiffEditorArguments(e.element.uri, e.element.originalUri, e.element.modifiedUri), e); + } - if (repository) { - this.scmViewService.focus(repository); + this.scmViewService.focus(e.element.historyItem.historyItemGroup.repository); + return; + } else if (isSCMHistoryItemChangeNode(e.element)) { + this.scmViewService.focus(e.element.context.historyItemGroup.repository); + return; } } @@ -2308,7 +2557,11 @@ export class SCMViewPane extends ViewPane { const menus = this.scmViewService.menus.getRepositoryMenus(element.provider); const menu = menus.getResourceGroupMenu(element); actions = collectContextMenuActions(menu); - } else if (ResourceTree.isResourceNode(element)) { + } else if (isSCMResource(element)) { + const menus = this.scmViewService.menus.getRepositoryMenus(element.resourceGroup.provider); + const menu = menus.getResourceMenu(element); + actions = collectContextMenuActions(menu); + } else if (isSCMResourceNode(element)) { if (element.element) { const menus = this.scmViewService.menus.getRepositoryMenus(element.element.resourceGroup.provider); const menu = menus.getResourceMenu(element.element); @@ -2318,10 +2571,14 @@ export class SCMViewPane extends ViewPane { const menu = menus.getResourceFolderMenu(element.context); actions = collectContextMenuActions(menu); } - } else { - const menus = this.scmViewService.menus.getRepositoryMenus(element.resourceGroup.provider); - const menu = menus.getResourceMenu(element); - actions = collectContextMenuActions(menu); + } else if (isSCMHistoryItemGroupTreeElement(element)) { + // TODO@lszomoru - wire up the context menu + } else if (isSCMHistoryItemTreeElement(element)) { + // TODO@lszomoru - wire up the context menu + } else if (isSCMHistoryItemChangeTreeElement(element)) { + // TODO@lszomoru - wire up the context menu + } else if (isSCMHistoryItemChangeNode(element)) { + // TODO@lszomoru - wire up the context menu } const actionRunner = new RepositoryPaneActionRunner(() => this.getSelectedResources()); @@ -2486,7 +2743,9 @@ class SCMTreeDataSource implements IAsyncDataSource ViewMode, private readonly alwaysShowRepositories: () => boolean, private readonly showActionButton: () => boolean, - @ISCMViewService private readonly scmViewService: ISCMViewService) { } + @ISCMViewService private readonly scmViewService: ISCMViewService, + @IUriIdentityService private uriIdentityService: IUriIdentityService, + ) { } hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean { if (isSCMViewService(inputOrElement)) { @@ -2503,12 +2762,18 @@ class SCMTreeDataSource implements IAsyncDataSource 0; + } else if (isSCMHistoryItemGroupTreeElement(inputOrElement)) { + return (inputOrElement.count ?? 0) > 0; + } else if (isSCMHistoryItemTreeElement(inputOrElement)) { + return true; + } else if (isSCMHistoryItemChangeTreeElement(inputOrElement)) { + return false; } else { throw new Error('hasChildren not implemented.'); } } - getChildren(inputOrElement: ISCMViewService | TreeElement): Iterable | Promise> { + async getChildren(inputOrElement: ISCMViewService | TreeElement): Promise> { const repositoryCount = this.scmViewService.visibleRepositories.length; const alwaysShowRepositories = this.alwaysShowRepositories(); @@ -2542,6 +2807,13 @@ class SCMTreeDataSource implements IAsyncDataSource { + const scmProvider = element.provider; + const historyProvider = scmProvider.historyProvider; + const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; + + if (!historyProvider || !currentHistoryItemGroup) { + return []; + } + + // History item group base + const historyItemGroupBase = await historyProvider.resolveHistoryItemGroupBase(currentHistoryItemGroup.id); + if (!historyItemGroupBase) { + return []; + } + + // Common ancestor, ahead, behind + const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor(currentHistoryItemGroup.id, historyItemGroupBase.id); + + const children: SCMHistoryItemGroupTreeElement[] = []; + // Incoming + if (historyItemGroupBase) { + children.push({ + id: historyItemGroupBase.id, + label: `$(cloud-download) ${historyItemGroupBase.label}`, + description: localize('incoming', "Incoming Changes"), + ancestor: ancestor?.id, + count: ancestor?.behind ?? 0, + repository: element, + type: 'historyItemGroup' + } as SCMHistoryItemGroupTreeElement); + } + + // Outgoing + children.push({ + id: currentHistoryItemGroup.id, + label: `$(cloud-upload) ${currentHistoryItemGroup.label}`, + description: localize('outgoing', "Outgoing Changes"), + ancestor: ancestor?.id, + count: ancestor?.ahead ?? 0, + repository: element, + type: 'historyItemGroup' + } as SCMHistoryItemGroupTreeElement); + + return children; + } + + private async getHistoryItems(element: SCMHistoryItemGroupTreeElement): Promise { + const scmProvider = element.repository.provider; + const historyProvider = scmProvider.historyProvider; + + if (!historyProvider) { + return []; + } + + const historyItems = await historyProvider.provideHistoryItems(element.id, { limit: { id: element.ancestor } }) ?? []; + return historyItems.map(historyItem => ({ + id: historyItem.id, + label: historyItem.label, + description: historyItem.description, + icon: historyItem.icon, + historyItemGroup: element, + type: 'historyItem' + } as SCMHistoryItemTreeElement)); + } + + private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode)[]> { + const repository = element.historyItemGroup.repository; + const historyProvider = repository.provider.historyProvider; + + if (!historyProvider) { + return []; + } + + // History Item Changes + const changes = await historyProvider.provideHistoryItemChanges(element.id) ?? []; + + if (this.viewMode() === ViewMode.List) { + // List + return changes.map(change => ({ + uri: change.uri, + originalUri: change.originalUri, + modifiedUri: change.modifiedUri, + renameUri: change.renameUri, + historyItem: element, + type: 'historyItemChange' + } as SCMHistoryItemChangeTreeElement)); + } + + // Tree + const tree = new ResourceTree(element, repository.provider.rootUri ?? URI.file('/'), this.uriIdentityService.extUri); + for (const change of changes) { + tree.add(change.uri, { + uri: change.uri, + originalUri: change.originalUri, + modifiedUri: change.modifiedUri, + renameUri: change.renameUri, + historyItem: element, + type: 'historyItemChange' + } as SCMHistoryItemChangeTreeElement); + } + + return [...tree.root.children]; + } + } export class SCMActionButton implements IDisposable { diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index ad83f6369bbea..50745a1076dbd 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as path from 'vs/base/common/path'; import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { IMenu } from 'vs/platform/actions/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -16,6 +17,9 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { Command } from 'vs/editor/common/languages'; import { reset } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMHistoryItemChangeTreeElement } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane'; +import { URI } from 'vs/base/common/uri'; +import { IResourceNode, ResourceTree } from 'vs/base/common/resourceTree'; export function isSCMRepositoryArray(element: any): element is ISCMRepository[] { return Array.isArray(element) && element.every(r => isSCMRepository(r)); @@ -45,6 +49,37 @@ export function isSCMResource(element: any): element is ISCMResource { return !!(element as ISCMResource).sourceUri && isSCMResourceGroup((element as ISCMResource).resourceGroup); } +export function isSCMResourceNode(element: any): element is IResourceNode { + return ResourceTree.isResourceNode(element) && isSCMResourceGroup(element.context); +} + +export function isSCMHistoryItemGroupTreeElement(element: any): element is SCMHistoryItemGroupTreeElement { + return (element as SCMHistoryItemGroupTreeElement).type === 'historyItemGroup'; +} + +export function isSCMHistoryItemTreeElement(element: any): element is SCMHistoryItemTreeElement { + return (element as SCMHistoryItemTreeElement).type === 'historyItem'; +} + +export function isSCMHistoryItemChangeTreeElement(element: any): element is SCMHistoryItemChangeTreeElement { + return (element as SCMHistoryItemChangeTreeElement).type === 'historyItemChange'; +} + +export function isSCMHistoryItemChangeNode(element: any): element is IResourceNode { + return ResourceTree.isResourceNode(element) && isSCMHistoryItemTreeElement(element.context); +} + +export function toDiffEditorArguments(uri: URI, originalUri: URI, modifiedUri: URI): unknown[] { + const basename = path.basename(uri.fsPath); + const originalQuery = JSON.parse(originalUri.query) as { path: string; ref: string }; + const modifiedQuery = JSON.parse(modifiedUri.query) as { path: string; ref: string }; + + const originalShortRef = originalQuery.ref.substring(0, 8).concat(originalQuery.ref.endsWith('^') ? '^' : ''); + const modifiedShortRef = modifiedQuery.ref.substring(0, 8).concat(modifiedQuery.ref.endsWith('^') ? '^' : ''); + + return [originalUri, modifiedUri, `${basename} (${originalShortRef}) ↔ ${basename} (${modifiedShortRef})`, null]; +} + const compareActions = (a: IAction, b: IAction) => a.id === b.id && a.enabled === b.enabled; export function connectPrimaryMenu(menu: IMenu, callback: (primary: IAction[], secondary: IAction[]) => void, primaryGroup?: string): IDisposable { From 12f730e5c6e7a08aed1aec39aa728c678a5cd7a2 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Mon, 6 Nov 2023 16:21:31 +0100 Subject: [PATCH 18/26] Added separator --- .../contrib/scm/browser/media/scm.css | 22 +++++- .../contrib/scm/browser/scmSyncViewPane.ts | 6 ++ .../contrib/scm/browser/scmViewPane.ts | 77 +++++++++++++++++-- src/vs/workbench/contrib/scm/browser/util.ts | 6 +- 4 files changed, 103 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 0d13de2bb2f38..9ceb67dff7441 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -144,18 +144,36 @@ padding-right: 4px; } -.scm-sync-view .monaco-list-row .history-item .monaco-icon-label .icon-container { +.scm-view .monaco-list-row .history-item .monaco-icon-label .icon-container { display: flex; font-size: 14px; padding-right: 4px; } -.scm-sync-view .monaco-list-row .history-item .monaco-icon-label .avatar { +.scm-view .monaco-list-row .history-item .monaco-icon-label .avatar { width: 14px; height: 14px; border-radius: 14px; } +.scm-view .monaco-list-row .separator-container { + display: flex; + align-items: center; + padding-left: 11px; +} + +.scm-view .monaco-list-row .separator-container .label-name { + font-size: 10px; +} + +.scm-view .monaco-list-row .separator-container .separator { + display: flex; + flex-grow: 1; + height: 0; + margin-left: 6px; + border-top: 1px solid var(--vscode-sideBar-border); +} + .scm-view .monaco-list-row .history > .name, .scm-view .monaco-list-row .history-item-group > .name, .scm-view .monaco-list-row .resource-group > .name { diff --git a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts index f21e8f03df0d7..bef5fb15b252e 100644 --- a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts @@ -135,6 +135,12 @@ export interface SCMHistoryItemChangeTreeElement extends ISCMHistoryItemChange { readonly type: 'historyItemChange'; } +export interface SCMViewSeparatorElement { + readonly label: string; + readonly repository: ISCMRepository; + readonly type: 'separator'; +} + class ListDelegate implements IListVirtualDelegate { getHeight(element: any): number { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index a6aa40d03a44b..e11ec69bcf09b 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -23,7 +23,7 @@ import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, import { IAction, ActionRunner, Action, Separator } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode } from './util'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService, isSCMHistoryItemGroupTreeElement, isSCMHistoryItemTreeElement, isSCMHistoryItemChangeTreeElement, toDiffEditorArguments, isSCMResourceNode, isSCMHistoryItemChangeNode, isSCMViewSeparator } from './util'; import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { disposableTimeout, ThrottledDelayer, Sequencer } from 'vs/base/common/async'; @@ -96,7 +96,7 @@ import { FormatOnType } from 'vs/editor/contrib/format/browser/formatActions'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IAsyncDataTreeViewState, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane'; +import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane'; import { stripIcons } from 'vs/base/common/iconLabels'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; @@ -112,7 +112,8 @@ type TreeElement = SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | SCMHistoryItemChangeTreeElement | - IResourceNode; + IResourceNode | + SCMViewSeparatorElement; interface ISCMLayout { height: number | undefined; @@ -844,6 +845,43 @@ class HistoryItemChangeRenderer implements ICompressibleTreeRenderer { + + static readonly TEMPLATE_ID = 'separator'; + get templateId(): string { return SeparatorRenderer.TEMPLATE_ID; } + + renderTemplate(container: HTMLElement): SeparatorTemplate { + // hack + (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-no-twistie'); + + // Use default cursor & disable hover for list item + container.parentElement!.parentElement!.classList.add('cursor-default', 'force-no-hover'); + + const element = append(container, $('.separator-container')); + const label = new IconLabel(element, { supportIcons: true, }); + append(element, $('.separator')); + + return { label, disposables: new DisposableStore() }; + } + renderElement(element: ITreeNode, index: number, templateData: SeparatorTemplate, height: number | undefined): void { + templateData.label.setLabel(element.element.label); + } + + renderCompressedElements(node: ITreeNode, void>, index: number, templateData: SeparatorTemplate, height: number | undefined): void { + throw new Error('Should never happen since node is incompressible'); + } + + disposeTemplate(templateData: SeparatorTemplate): void { + throw new Error('Method not implemented.'); + } + +} + class ListDelegate implements IListVirtualDelegate { constructor(private readonly inputRenderer: InputRenderer) { } @@ -875,6 +913,8 @@ class ListDelegate implements IListVirtualDelegate { return HistoryItemRenderer.TEMPLATE_ID; } else if (isSCMHistoryItemChangeTreeElement(element) || isSCMHistoryItemChangeNode(element)) { return HistoryItemChangeRenderer.TEMPLATE_ID; + } else if (isSCMViewSeparator(element)) { + return SeparatorRenderer.TEMPLATE_ID; } else { throw new Error('Unknown element'); } @@ -902,6 +942,8 @@ class SCMTreeFilter implements ITreeFilter { return element.resources.length > 0 || !element.hideWhenEmpty; } else if (isSCMHistoryItemGroupTreeElement(element)) { return (element.count ?? 0) > 0; + } else if (isSCMViewSeparator(element)) { + return element.repository.provider.groups.some(g => g.resources.length > 0); } else { return true; } @@ -935,6 +977,14 @@ export class SCMTreeSorter implements ITreeSorter { return 1; } + if (isSCMViewSeparator(one)) { + if (!isSCMHistoryItemGroupTreeElement(other) && !isSCMResourceGroup(other)) { + throw new Error('Invalid comparison'); + } + + return 0; + } + if (isSCMResourceGroup(one)) { if (!isSCMResourceGroup(other)) { throw new Error('Invalid comparison'); @@ -944,7 +994,7 @@ export class SCMTreeSorter implements ITreeSorter { } if (isSCMHistoryItemGroupTreeElement(one)) { - if (!isSCMHistoryItemGroupTreeElement(other) && !isSCMResourceGroup(other)) { + if (!isSCMHistoryItemGroupTreeElement(other) && !isSCMResourceGroup(other) && !isSCMViewSeparator(other)) { throw new Error('Invalid comparison'); } @@ -1019,6 +1069,8 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb return element.label; } else if (isSCMHistoryItemTreeElement(element)) { return element.label; + } else if (isSCMViewSeparator(element)) { + return element.label; } else { if (this.viewMode() === ViewMode.List) { // In List mode match using the file name and the path. @@ -1077,6 +1129,9 @@ function getSCMResourceId(element: TreeElement): string { const historyItemGroup = historyItem.historyItemGroup; const provider = historyItemGroup.repository.provider; return `folder:${provider.id}/${historyItemGroup.id}/${historyItem.id}/$FOLDER/${element.uri.toString()}`; + } else if (isSCMViewSeparator(element)) { + const provider = element.repository.provider; + return `separator:${provider.id}`; } else { throw new Error('Invalid tree element'); } @@ -1134,6 +1189,8 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider this.viewMode, this.listLabels), + this.instantiationService.createInstance(SeparatorRenderer) ], this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode, () => this.alwaysShowRepositories, () => this.showActionButton), { @@ -2768,6 +2826,8 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Tue, 7 Nov 2023 11:04:34 +0100 Subject: [PATCH 19/26] Fix some issues in sorting tree nodes --- .../contrib/scm/browser/scmViewPane.ts | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index e11ec69bcf09b..bea48e09f4426 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1009,7 +1009,28 @@ export class SCMTreeSorter implements ITreeSorter { return 0; } - // List + if (isSCMHistoryItemChangeTreeElement(one) || isSCMHistoryItemChangeNode(one)) { + // List + if (this.viewMode() === ViewMode.List) { + if (!isSCMHistoryItemChangeTreeElement(other)) { + throw new Error('Invalid comparison'); + } + + return comparePaths(one.uri.fsPath, other.uri.fsPath); + } + + // Tree + if (!isSCMHistoryItemChangeTreeElement(other) && !isSCMHistoryItemChangeNode(other)) { + throw new Error('Invalid comparison'); + } + + const oneName = isSCMHistoryItemChangeNode(one) ? one.name : basename(one.uri); + const otherName = isSCMHistoryItemChangeNode(other) ? other.name : basename(other.uri); + + return compareFileNames(oneName, otherName); + } + + // Resource (List) if (this.viewMode() === ViewMode.List) { // FileName if (this.viewSortKey() === ViewSortKey.Name) { @@ -1036,7 +1057,7 @@ export class SCMTreeSorter implements ITreeSorter { return comparePaths(onePath, otherPath); } - // Tree + // Resource (Tree) const oneIsDirectory = ResourceTree.isResourceNode(one); const otherIsDirectory = ResourceTree.isResourceNode(other); @@ -2943,7 +2964,7 @@ class SCMTreeDataSource implements IAsyncDataSource ({ - id: historyItem.id, - label: historyItem.label, - description: historyItem.description, - icon: historyItem.icon, + ...historyItem, historyItemGroup: element, type: 'historyItem' - } as SCMHistoryItemTreeElement)); + })); } private async getHistoryItemChanges(element: SCMHistoryItemTreeElement): Promise<(SCMHistoryItemChangeTreeElement | IResourceNode)[]> { @@ -2993,26 +3011,20 @@ class SCMTreeDataSource implements IAsyncDataSource ({ - uri: change.uri, - originalUri: change.originalUri, - modifiedUri: change.modifiedUri, - renameUri: change.renameUri, + ...change, historyItem: element, type: 'historyItemChange' - } as SCMHistoryItemChangeTreeElement)); + })); } // Tree const tree = new ResourceTree(element, repository.provider.rootUri ?? URI.file('/'), this.uriIdentityService.extUri); for (const change of changes) { tree.add(change.uri, { - uri: change.uri, - originalUri: change.originalUri, - modifiedUri: change.modifiedUri, - renameUri: change.renameUri, + ...change, historyItem: element, type: 'historyItemChange' - } as SCMHistoryItemChangeTreeElement); + }); } return [...tree.root.children]; From 4aa1dd1d1441f3f99c44e8f609ab764ed5d2673c Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 7 Nov 2023 13:34:59 +0100 Subject: [PATCH 20/26] Only show separator if there are incoming/outgoing history item groups --- .../contrib/scm/browser/scmViewPane.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index bea48e09f4426..409b5175af232 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2888,18 +2888,21 @@ class SCMTreeDataSource implements IAsyncDataSource h.count ?? 0 > 0)) { + // Separator + children.push({ + label: 'Incoming/Outgoing', + repository: inputOrElement, + type: 'separator' + } as SCMViewSeparatorElement); + } + + children.push(...historyItemGroups); } return children; From 63694cc7e674980162171671641f7f0ba3f1a387 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:19:30 +0100 Subject: [PATCH 21/26] Remove sync view registration, add setting --- .../contrib/scm/browser/scm.contribution.ts | 38 ++++++++++--------- .../contrib/scm/browser/scmViewPane.ts | 38 +++++++++++++------ 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 5df3f7ae41d56..efa226895b4a0 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -7,7 +7,7 @@ import { localize, localize2 } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { DirtyDiffWorkbenchController } from './dirtydiffDecorator'; -import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID, SYNC_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; +import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { SCMActiveResourceContextKeyController, SCMStatusController } from './activity'; @@ -32,7 +32,6 @@ import { Context as SuggestContext } from 'vs/editor/contrib/suggest/browser/sug import { MANAGE_TRUST_COMMAND_ID, WorkspaceTrustContext } from 'vs/workbench/contrib/workspace/common/workspace'; import { IQuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiff'; import { QuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiffService'; -import { SCMSyncViewPane } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane'; import { getActiveElement } from 'vs/base/browser/dom'; ModesRegistry.registerLanguage({ @@ -81,7 +80,7 @@ viewsRegistry.registerViews([{ ctorDescriptor: new SyncDescriptor(SCMViewPane), canToggleVisibility: true, canMoveView: true, - weight: 60, + weight: 80, order: -999, containerIcon: sourceControlViewIcon, openCommandActionDescriptor: { @@ -111,17 +110,6 @@ viewsRegistry.registerViews([{ containerIcon: sourceControlViewIcon }], viewContainer); -viewsRegistry.registerViews([{ - id: SYNC_VIEW_PANE_ID, - name: localize2('source control sync', "Source Control Sync"), - ctorDescriptor: new SyncDescriptor(SCMSyncViewPane), - canToggleVisibility: true, - canMoveView: true, - weight: 20, - order: -998, - when: ContextKeyExpr.equals('config.scm.experimental.showSyncView', true), -}], viewContainer); - Registry.as(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(SCMActiveResourceContextKeyController, LifecyclePhase.Restored); @@ -298,10 +286,24 @@ Registry.as(ConfigurationExtensions.Configuration).regis markdownDescription: localize('showActionButton', "Controls whether an action button can be shown in the Source Control view."), default: true }, - 'scm.experimental.showSyncView': { - type: 'boolean', - description: localize('showSyncView', "Controls whether the Source Control Sync view is shown."), - default: false + 'scm.experimental.showSyncInformation': { + type: 'object', + description: localize('showSyncInformation', "Controls whether incoming/outgoing changes are shown in the Source Control view."), + additionalProperties: false, + properties: { + 'incoming': { + type: 'boolean', + description: localize('showSyncInformationIncoming', "Show incoming changes in the Source Control view."), + }, + 'outgoing': { + type: 'boolean', + description: localize('showSyncInformationOutgoing', "Show outgoing changes in the Source Control view."), + }, + }, + default: { + 'incoming': false, + 'outgoing': false + } } } }); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 409b5175af232..11abed136d48c 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2219,6 +2219,9 @@ export class SCMViewPane extends ViewPane { private _alwaysShowRepositories = false; get alwaysShowRepositories(): boolean { return this._alwaysShowRepositories; } + private _showSyncInformation: { incoming: boolean; outgoing: boolean } = { incoming: false, outgoing: false }; + get showSyncInformation(): { incoming: boolean; outgoing: boolean } { return this._showSyncInformation; } + private readonly items = new DisposableMap(); private readonly visibilityDisposables = new DisposableStore(); private readonly asyncOperationSequencer = new Sequencer(); @@ -2346,6 +2349,14 @@ export class SCMViewPane extends ViewPane { Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.visibilityDisposables)(updateRepositoryVisibility, this, this.visibilityDisposables); updateRepositoryVisibility(); + const updateSyncInformationVisibility = () => { + const setting = this.configurationService.getValue<{ incoming: boolean; outgoing: boolean }>('scm.experimental.showSyncInformation'); + this._showSyncInformation = { incoming: setting.incoming === true, outgoing: setting.outgoing === true }; + this.updateChildren(); + }; + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.experimental.showSyncInformation'), this.visibilityDisposables)(updateSyncInformationVisibility, this, this.visibilityDisposables); + updateSyncInformationVisibility(); + // Add visible repositories this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.visibilityDisposables); this.onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); @@ -2404,7 +2415,7 @@ export class SCMViewPane extends ViewPane { this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this.viewMode, this.listLabels), this.instantiationService.createInstance(SeparatorRenderer) ], - this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode, () => this.alwaysShowRepositories, () => this.showActionButton), + this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode, () => this.alwaysShowRepositories, () => this.showActionButton, () => this.showSyncInformation), { horizontalScrolling: false, setRowLineHeight: false, @@ -2822,6 +2833,7 @@ class SCMTreeDataSource implements IAsyncDataSource ViewMode, private readonly alwaysShowRepositories: () => boolean, private readonly showActionButton: () => boolean, + private readonly showSyncInformation: () => { incoming: boolean; outgoing: boolean }, @ISCMViewService private readonly scmViewService: ISCMViewService, @IUriIdentityService private uriIdentityService: IUriIdentityService, ) { } @@ -2943,7 +2955,7 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Wed, 8 Nov 2023 16:51:27 +0100 Subject: [PATCH 22/26] Some more polish based on feedback --- extensions/git/src/historyProvider.ts | 7 +++++-- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index d3a64bb4bb52b..a7fd51cef9318 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -10,6 +10,7 @@ import { IDisposable } from './util'; import { toGitUri } from './uri'; import { SyncActionButton } from './actionButton'; import { RefType, Status } from './api/git'; +import { emojify, ensureEmojis } from './emoji'; export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable { @@ -83,6 +84,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec this.getSummaryHistoryItem(optionsRef, historyItemGroupIdRef) ]); + await ensureEmojis(); + const historyItems = commits.length === 0 ? [] : [summary]; historyItems.push(...commits.map(commit => { const newLineIndex = commit.message.indexOf('\n'); @@ -91,9 +94,9 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return { id: commit.hash, parentIds: commit.parents, - label: subject, + label: emojify(subject), description: commit.authorName, - icon: new ThemeIcon('account'), + icon: new ThemeIcon('git-commit'), timestamp: commit.authorDate?.getTime() }; })); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 11abed136d48c..e1ac53a3428e0 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2974,7 +2974,7 @@ class SCMTreeDataSource implements IAsyncDataSource Date: Wed, 8 Nov 2023 17:04:54 +0100 Subject: [PATCH 23/26] Add back view registration, setting, and localization --- .../contrib/scm/browser/scm.contribution.ts | 19 +++++++++++- .../contrib/scm/browser/scmViewPane.ts | 29 ++++++++++--------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index efa226895b4a0..0c11c5ecebd6a 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -7,7 +7,7 @@ import { localize, localize2 } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { DirtyDiffWorkbenchController } from './dirtydiffDecorator'; -import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; +import { VIEWLET_ID, ISCMService, VIEW_PANE_ID, ISCMProvider, ISCMViewService, REPOSITORIES_VIEW_PANE_ID, SYNC_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { SCMActiveResourceContextKeyController, SCMStatusController } from './activity'; @@ -33,6 +33,7 @@ import { MANAGE_TRUST_COMMAND_ID, WorkspaceTrustContext } from 'vs/workbench/con import { IQuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiff'; import { QuickDiffService } from 'vs/workbench/contrib/scm/common/quickDiffService'; import { getActiveElement } from 'vs/base/browser/dom'; +import { SCMSyncViewPane } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane'; ModesRegistry.registerLanguage({ id: 'scminput', @@ -110,6 +111,17 @@ viewsRegistry.registerViews([{ containerIcon: sourceControlViewIcon }], viewContainer); +viewsRegistry.registerViews([{ + id: SYNC_VIEW_PANE_ID, + name: localize2('source control sync', "Source Control Sync"), + ctorDescriptor: new SyncDescriptor(SCMSyncViewPane), + canToggleVisibility: true, + canMoveView: true, + weight: 20, + order: -998, + when: ContextKeyExpr.equals('config.scm.experimental.showSyncView', true), +}], viewContainer); + Registry.as(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(SCMActiveResourceContextKeyController, LifecyclePhase.Restored); @@ -286,6 +298,11 @@ Registry.as(ConfigurationExtensions.Configuration).regis markdownDescription: localize('showActionButton', "Controls whether an action button can be shown in the Source Control view."), default: true }, + 'scm.experimental.showSyncView': { + type: 'boolean', + description: localize('showSyncView', "Controls whether the Source Control Sync view is shown."), + default: false + }, 'scm.experimental.showSyncInformation': { type: 'object', description: localize('showSyncInformation', "Controls whether incoming/outgoing changes are shown in the Source Control view."), diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index e1ac53a3428e0..a917e08b3b348 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2901,20 +2901,23 @@ class SCMTreeDataSource implements IAsyncDataSource h.count ?? 0 > 0)) { - // Separator - children.push({ - label: 'Incoming/Outgoing', - repository: inputOrElement, - type: 'separator' - } as SCMViewSeparatorElement); - } + if (this.showSyncInformation().incoming || this.showSyncInformation().outgoing) { + const historyProvider = inputOrElement.provider.historyProvider; + const historyItemGroup = historyProvider?.currentHistoryItemGroup; + + if (historyProvider && historyItemGroup) { + const historyItemGroups = await this.getHistoryItemGroups(inputOrElement); + if (historyItemGroups.some(h => h.count ?? 0 > 0)) { + // Separator + children.push({ + label: localize('syncSeparatorHeader', "Incoming/Outgoing"), + repository: inputOrElement, + type: 'separator' + } as SCMViewSeparatorElement); + } - children.push(...historyItemGroups); + children.push(...historyItemGroups); + } } return children; From 1d22b2c1fe1cb550880a341fcd948e02b880393b Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:55:14 +0100 Subject: [PATCH 24/26] Code cleanup --- .../contrib/scm/browser/scmViewPane.ts | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index a917e08b3b348..7c14020cfde55 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2508,13 +2508,9 @@ export class SCMViewPane extends ViewPane { return; } else if (isSCMHistoryItemGroupTreeElement(e.element)) { this.scmViewService.focus(e.element.repository); - - // TODO@lszomoru: focus the history item group return; } else if (isSCMHistoryItemTreeElement(e.element)) { this.scmViewService.focus(e.element.historyItemGroup.repository); - - // TODO@lszomoru: focus the history item return; } else if (isSCMHistoryItemChangeTreeElement(e.element)) { if (e.element.originalUri && e.element.modifiedUri) { @@ -2661,15 +2657,16 @@ export class SCMViewPane extends ViewPane { const menu = menus.getResourceFolderMenu(element.context); actions = collectContextMenuActions(menu); } - } else if (isSCMHistoryItemGroupTreeElement(element)) { - // TODO@lszomoru - wire up the context menu - } else if (isSCMHistoryItemTreeElement(element)) { - // TODO@lszomoru - wire up the context menu - } else if (isSCMHistoryItemChangeTreeElement(element)) { - // TODO@lszomoru - wire up the context menu - } else if (isSCMHistoryItemChangeNode(element)) { - // TODO@lszomoru - wire up the context menu } + // else if (isSCMHistoryItemGroupTreeElement(element)) { + // // TODO@lszomoru - wire up the context menu + // } else if (isSCMHistoryItemTreeElement(element)) { + // // TODO@lszomoru - wire up the context menu + // } else if (isSCMHistoryItemChangeTreeElement(element)) { + // // TODO@lszomoru - wire up the context menu + // } else if (isSCMHistoryItemChangeNode(element)) { + // // TODO@lszomoru - wire up the context menu + // } const actionRunner = new RepositoryPaneActionRunner(() => this.getSelectedResources()); actionRunner.onWillRun(() => this.tree.domFocus()); From 3eef0e2984f1a324be4bba23130ff928c76613fc Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:56:05 +0100 Subject: [PATCH 25/26] Delete code that was commented out --- src/vs/workbench/contrib/scm/browser/scmViewPane.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 7c14020cfde55..533b9b5432b8f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2658,15 +2658,6 @@ export class SCMViewPane extends ViewPane { actions = collectContextMenuActions(menu); } } - // else if (isSCMHistoryItemGroupTreeElement(element)) { - // // TODO@lszomoru - wire up the context menu - // } else if (isSCMHistoryItemTreeElement(element)) { - // // TODO@lszomoru - wire up the context menu - // } else if (isSCMHistoryItemChangeTreeElement(element)) { - // // TODO@lszomoru - wire up the context menu - // } else if (isSCMHistoryItemChangeNode(element)) { - // // TODO@lszomoru - wire up the context menu - // } const actionRunner = new RepositoryPaneActionRunner(() => this.getSelectedResources()); actionRunner.onWillRun(() => this.tree.domFocus()); From 6351fc7fc71367bd506c0b2b5e154f43c5912a91 Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 8 Nov 2023 22:08:33 +0100 Subject: [PATCH 26/26] Move type definitions --- .../contrib/scm/browser/scmSyncViewPane.ts | 26 +-------------- .../contrib/scm/browser/scmViewPane.ts | 2 +- src/vs/workbench/contrib/scm/browser/util.ts | 2 +- .../workbench/contrib/scm/common/history.ts | 32 ++++++++++++++++--- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts index bef5fb15b252e..4805711e193aa 100644 --- a/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmSyncViewPane.ts @@ -32,7 +32,6 @@ import { ActionButtonRenderer } from 'vs/workbench/contrib/scm/browser/scmViewPa import { getActionViewItemProvider, isSCMActionButton, isSCMRepository, isSCMRepositoryArray } from 'vs/workbench/contrib/scm/browser/util'; import { ISCMActionButton, ISCMRepository, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, SYNC_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { comparePaths } from 'vs/base/common/comparers'; -import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup } from 'vs/workbench/contrib/scm/common/history'; import { localize } from 'vs/nls'; import { Iterable } from 'vs/base/common/iterator'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; @@ -50,6 +49,7 @@ import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { IResourceNode, ResourceTree } from 'vs/base/common/resourceTree'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement } from 'vs/workbench/contrib/scm/common/history'; type SCMHistoryItemChangeResourceTreeNode = IResourceNode; type TreeElement = ISCMRepository[] | ISCMRepository | ISCMActionButton | SCMHistoryItemGroupTreeElement | SCMHistoryItemTreeElement | SCMHistoryItemChangeTreeElement | SCMHistoryItemChangeResourceTreeNode; @@ -117,30 +117,6 @@ const ContextKeys = { ViewMode: new RawContextKey('scmSyncViewMode', ViewMode.List), }; -export interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup { - readonly description?: string; - readonly ancestor?: string; - readonly count?: number; - readonly repository: ISCMRepository; - readonly type: 'historyItemGroup'; -} - -export interface SCMHistoryItemTreeElement extends ISCMHistoryItem { - readonly historyItemGroup: SCMHistoryItemGroupTreeElement; - readonly type: 'historyItem'; -} - -export interface SCMHistoryItemChangeTreeElement extends ISCMHistoryItemChange { - readonly historyItem: SCMHistoryItemTreeElement; - readonly type: 'historyItemChange'; -} - -export interface SCMViewSeparatorElement { - readonly label: string; - readonly repository: ISCMRepository; - readonly type: 'separator'; -} - class ListDelegate implements IListVirtualDelegate { getHeight(element: any): number { diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 533b9b5432b8f..6af291540613f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -10,6 +10,7 @@ import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode, prepend } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; +import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton, ISCMActionButtonDescriptor, ISCMRepositorySortKey, REPOSITORIES_VIEW_PANE_ID } from 'vs/workbench/contrib/scm/common/scm'; import { ResourceLabels, IResourceLabel, IFileLabelOptions } from 'vs/workbench/browser/labels'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; @@ -96,7 +97,6 @@ import { FormatOnType } from 'vs/editor/contrib/format/browser/formatActions'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IAsyncDataTreeViewState, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane'; import { stripIcons } from 'vs/base/common/iconLabels'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index a77a7a936fc1b..f98f68c4bb4da 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'vs/base/common/path'; +import { SCMHistoryItemChangeTreeElement, SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/common/history'; import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { IMenu } from 'vs/platform/actions/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -17,7 +18,6 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { Command } from 'vs/editor/common/languages'; import { reset } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { SCMHistoryItemGroupTreeElement, SCMHistoryItemTreeElement, SCMHistoryItemChangeTreeElement, SCMViewSeparatorElement } from 'vs/workbench/contrib/scm/browser/scmSyncViewPane'; import { URI } from 'vs/base/common/uri'; import { IResourceNode, ResourceTree } from 'vs/base/common/resourceTree'; diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index b9f6792c9ea0c..3bee76802bf78 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; -import { ISCMActionButtonDescriptor } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMActionButtonDescriptor, ISCMRepository } from 'vs/workbench/contrib/scm/common/scm'; export interface ISCMHistoryProvider { @@ -30,15 +30,23 @@ export interface ISCMHistoryOptions { readonly limit?: number | { id?: string }; } -export interface ISCMHistoryItemGroup { +export interface ISCMRemoteHistoryItemGroup { readonly id: string; readonly label: string; - readonly upstream?: ISCMRemoteHistoryItemGroup; } -export interface ISCMRemoteHistoryItemGroup { +export interface ISCMHistoryItemGroup { readonly id: string; readonly label: string; + readonly upstream?: ISCMRemoteHistoryItemGroup; +} + +export interface SCMHistoryItemGroupTreeElement extends ISCMHistoryItemGroup { + readonly description?: string; + readonly ancestor?: string; + readonly count?: number; + readonly repository: ISCMRepository; + readonly type: 'historyItemGroup'; } export interface ISCMHistoryItem { @@ -50,9 +58,25 @@ export interface ISCMHistoryItem { readonly timestamp?: number; } +export interface SCMHistoryItemTreeElement extends ISCMHistoryItem { + readonly historyItemGroup: SCMHistoryItemGroupTreeElement; + readonly type: 'historyItem'; +} + export interface ISCMHistoryItemChange { readonly uri: URI; readonly originalUri?: URI; readonly modifiedUri?: URI; readonly renameUri?: URI; } + +export interface SCMHistoryItemChangeTreeElement extends ISCMHistoryItemChange { + readonly historyItem: SCMHistoryItemTreeElement; + readonly type: 'historyItemChange'; +} + +export interface SCMViewSeparatorElement { + readonly label: string; + readonly repository: ISCMRepository; + readonly type: 'separator'; +}