diff --git a/extensions/git/package.json b/extensions/git/package.json index d2f3f62966e..7caad7799aa 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -527,6 +527,7 @@ { "command": "git.fetchAll", "title": "%command.fetchAll%", + "icon": "$(git-fetch)", "category": "Git", "enablement": "!operationInProgress" }, @@ -1936,24 +1937,9 @@ ], "scm/history/title": [ { - "command": "git.fetchRef", + "command": "git.fetchAll", "group": "navigation@1", - "when": "scmProvider == git && scmHistoryItemGroupHasRemote" - }, - { - "command": "git.pullRef", - "group": "navigation@2", - "when": "scmProvider == git && scmHistoryItemGroupHasRemote" - }, - { - "command": "git.pushRef", - "when": "scmProvider == git && scmHistoryItemGroupHasRemote", - "group": "navigation@3" - }, - { - "command": "git.publish", - "when": "scmProvider == git && !scmHistoryItemGroupHasRemote", - "group": "navigation@3" + "when": "scmProvider == git" } ], "scm/historyItem/context": [ diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 7f972b9c4f3..54108d7212f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -4,12 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import './media/scm.css'; -import { Event } from '../../../../base/common/event.js'; import * as platform from '../../../../base/common/platform.js'; import { $, append } from '../../../../base/browser/dom.js'; -import { IHoverOptions, IManagedHover, IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; +import { IHoverOptions, IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; -import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { LabelFuzzyScore } from '../../../../base/browser/ui/tree/abstractTree.js'; @@ -17,8 +15,8 @@ import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode, ITreeRenderer } fro import { fromNow } from '../../../../base/common/date.js'; import { createMatches, FuzzyScore, IMatch } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, autorunWithStore, observableValue } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -36,32 +34,31 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IViewPaneOptions, ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; import { renderSCMHistoryItemGraph, historyItemGroupLocal, historyItemGroupRemote, historyItemGroupBase, historyItemGroupHoverLabelForeground, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder } from './scmHistory.js'; -import { RepositoryActionRunner } from './scmRepositoryRenderer.js'; -import { collectContextMenuActions, connectPrimaryMenu, getActionViewItemProvider, isSCMHistoryItemLoadMoreTreeElement, isSCMHistoryItemViewModelTreeElement, isSCMRepository, isSCMViewService } from './util.js'; +import { collectContextMenuActions, isSCMHistoryItemLoadMoreTreeElement, isSCMHistoryItemViewModelTreeElement, isSCMRepository } from './util.js'; import { ISCMHistoryItem, ISCMHistoryItemGroup, ISCMHistoryItemViewModel, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js'; -import { HISTORY_VIEW_PANE_ID, ISCMProvider, ISCMRepository, ISCMService, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent } from '../common/scm.js'; +import { HISTORY_VIEW_PANE_ID, ISCMProvider, ISCMRepository, ISCMService, ISCMViewService } from '../common/scm.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; import { stripIcons } from '../../../../base/common/iconLabels.js'; import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; -import { Action2, IMenuService, MenuId, MenuItemAction, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { Iterable } from '../../../../base/common/iterator.js'; +import { Action2, MenuId, MenuItemAction, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { Sequencer, Throttler } from '../../../../base/common/async.js'; import { URI } from '../../../../base/common/uri.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ActionRunner, IAction, IActionRunner } from '../../../../base/common/actions.js'; import { tail } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { ContextKeys } from './scmViewPane.js'; -import { IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; -import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IProgressService } from '../../../../platform/progress/common/progress.js'; +import { derivedObservableWithCache, latestChangedValue, observableFromEvent, observableFromEventOpts } from '../../../../base/common/observableInternal/utils.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { EditorResourceAccessor } from '../../../common/editor.js'; +import { ContextKeys } from './scmViewPane.js'; registerColor('scm.historyItemStatisticsBorder', transparent(foreground, 0.2), localize('scm.historyItemStatisticsBorder', "History item statistics border color.")); const historyItemAdditionsForeground = registerColor('scm.historyItemAdditionsForeground', 'gitDecoration.addedResourceForeground', localize('scm.historyItemAdditionsForeground', "History item additions foreground color.")); const historyItemDeletionsForeground = registerColor('scm.historyItemDeletionsForeground', 'gitDecoration.deletedResourceForeground', localize('scm.historyItemDeletionsForeground', "History item deletions foreground color.")); -type TreeElement = ISCMRepository | SCMHistoryItemViewModelTreeElement | SCMHistoryItemLoadMoreTreeElement; +type TreeElement = SCMHistoryItemViewModelTreeElement | SCMHistoryItemLoadMoreTreeElement; registerAction2(class extends ViewAction { constructor() { @@ -73,21 +70,14 @@ registerAction2(class extends ViewAction { icon: Codicon.refresh, menu: { id: MenuId.SCMHistoryTitle, - when: ContextKeyExpr.or( - ContextKeyExpr.has('scmRepository'), - ContextKeyExpr.and(ContextKeys.RepositoryVisibilityCount.isEqualTo(1), ContextKeyExpr.equals('config.scm.alwaysShowRepositories', false)) - ), group: 'navigation', order: 1000 } }); } - async runInView(accessor: ServicesAccessor, view: SCMHistoryViewPane, provider?: ISCMProvider): Promise { - const scmService = accessor.get(ISCMService); - const repository = provider ? scmService.getRepository(provider.id) : undefined; - - view.refresh(repository); + async runInView(_: ServicesAccessor, view: SCMHistoryViewPane): Promise { + view.refresh(); } }); @@ -151,9 +141,7 @@ class ListDelegate implements IListVirtualDelegate { } getTemplateId(element: TreeElement): string { - if (isSCMRepository(element)) { - return RepositoryRenderer.TEMPLATE_ID; - } else if (isSCMHistoryItemViewModelTreeElement(element)) { + if (isSCMHistoryItemViewModelTreeElement(element)) { return HistoryItemRenderer.TEMPLATE_ID; } else if (isSCMHistoryItemLoadMoreTreeElement(element)) { return HistoryItemLoadMoreRenderer.TEMPLATE_ID; @@ -163,91 +151,6 @@ class ListDelegate implements IListVirtualDelegate { } } -interface RepositoryTemplate { - readonly label: IconLabel; - readonly labelCustomHover: IManagedHover; - readonly stateLabel: HTMLElement; - readonly toolBar: WorkbenchToolBar; - readonly elementDisposables: DisposableStore; - readonly templateDisposable: IDisposable; -} - -class RepositoryRenderer implements ITreeRenderer { - - static readonly TEMPLATE_ID = 'repository'; - get templateId(): string { return RepositoryRenderer.TEMPLATE_ID; } - - constructor( - private readonly description: (repository: ISCMRepository) => IObservable, - private readonly actionRunner: IActionRunner, - private readonly actionViewItemProvider: IActionViewItemProvider, - @ICommandService private commandService: ICommandService, - @IContextKeyService private contextKeyService: IContextKeyService, - @IContextMenuService private contextMenuService: IContextMenuService, - @IHoverService private hoverService: IHoverService, - @IKeybindingService private keybindingService: IKeybindingService, - @IMenuService private menuService: IMenuService, - @ITelemetryService private telemetryService: ITelemetryService - ) { } - - renderTemplate(container: HTMLElement): RepositoryTemplate { - // hack - if (container.classList.contains('monaco-tl-contents')) { - (container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement).classList.add('force-twistie'); - } - - const element = append(container, $('.scm-provider')); - const label = new IconLabel(element); - const labelCustomHover = this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), label.element, '', {}); - const stateLabel = append(element, $('div.state-label.monaco-count-badge.long')); - const toolBar = new WorkbenchToolBar(append(element, $('.actions')), { actionRunner: this.actionRunner, actionViewItemProvider: this.actionViewItemProvider, resetMenu: MenuId.SCMHistoryTitle }, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService); - - return { label, labelCustomHover, stateLabel, toolBar, elementDisposables: new DisposableStore(), templateDisposable: combinedDisposable(labelCustomHover, toolBar) }; - } - - renderElement(arg: ISCMRepository | ITreeNode, index: number, templateData: RepositoryTemplate, height: number | undefined): void { - const repository = isSCMRepository(arg) ? arg : arg.element; - - templateData.elementDisposables.add(autorun(reader => { - const description = this.description(repository).read(reader); - templateData.stateLabel.style.display = description !== '' ? '' : 'none'; - templateData.stateLabel.textContent = description; - })); - - templateData.label.setLabel(repository.provider.name); - templateData.labelCustomHover.update(repository.provider.rootUri ? `${repository.provider.label}: ${repository.provider.rootUri.fsPath}` : repository.provider.label); - - templateData.elementDisposables.add(autorunWithStore((reader, store) => { - const currentHistoryItemGroup = repository.provider.historyProvider.read(reader)?.currentHistoryItemGroup.read(reader); - if (!currentHistoryItemGroup) { - templateData.toolBar.setActions([], []); - return; - } - - const contextKeyService = this.contextKeyService.createOverlay([ - ['scmRepository', repository.id], - ['scmProvider', repository.provider.contextValue], - ['scmHistoryItemGroupHasRemote', !!currentHistoryItemGroup.remote], - ]); - const menu = this.menuService.createMenu(MenuId.SCMHistoryTitle, contextKeyService); - store.add(connectPrimaryMenu(menu, (primary, secondary) => { - templateData.toolBar.setActions(primary, secondary); - })); - })); - - templateData.toolBar.context = repository.provider; - } - - disposeElement(group: ISCMRepository | ITreeNode, index: number, template: RepositoryTemplate): void { - template.elementDisposables.clear(); - } - - disposeTemplate(templateData: RepositoryTemplate): void { - templateData.elementDisposables.dispose(); - templateData.templateDisposable.dispose(); - } -} - interface HistoryItemTemplate { readonly element: HTMLElement; readonly label: IconLabel; @@ -428,10 +331,9 @@ class HistoryItemLoadMoreRenderer implements ITreeRenderer IObservable, - private readonly _loadMoreCallback: (repository: ISCMRepository) => void, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ISCMViewService private readonly _scmViewService: ISCMViewService) { } + // private readonly _loadingMore: () => IObservable, + private readonly _loadMoreCallback: (repository: ISCMRepository) => void + ) { } renderTemplate(container: HTMLElement): LoadMoreTemplate { // hack @@ -446,26 +348,23 @@ class HistoryItemLoadMoreRenderer implements ITreeRenderer, index: number, templateData: LoadMoreTemplate, height: number | undefined): void { - const repositoryCount = this._scmViewService.visibleRepositories.length; - const alwaysShowRepositories = this._configurationService.getValue('scm.alwaysShowRepositories') === true; - templateData.graphPlaceholder.textContent = ''; templateData.graphPlaceholder.style.width = `${SWIMLANE_WIDTH * (element.element.graphColumns.length + 1)}px`; templateData.graphPlaceholder.appendChild(renderSCMHistoryGraphPlaceholder(element.element.graphColumns)); - templateData.historyItemPlaceholderContainer.classList.toggle('shimmer', repositoryCount === 1 && !alwaysShowRepositories); + templateData.historyItemPlaceholderContainer.classList.toggle('shimmer', true); - if (repositoryCount > 1 || alwaysShowRepositories) { - templateData.elementDisposables.add(autorun(reader => { - const loadingMore = this._loadingMore(element.element.repository).read(reader); - const icon = `$(${loadingMore ? 'loading~spin' : 'fold-down'})`; + // if (repositoryCount > 1 || alwaysShowRepositories) { + // templateData.elementDisposables.add(autorun(reader => { + // const loadingMore = this._loadingMore().read(reader); + // const icon = `$(${loadingMore ? 'loading~spin' : 'fold-down'})`; - templateData.historyItemPlaceholderLabel.setLabel(localize('loadMore', "{0} Load More...", icon)); - })); - } else { - templateData.historyItemPlaceholderLabel.setLabel(''); - this._loadMoreCallback(element.element.repository); - } + // templateData.historyItemPlaceholderLabel.setLabel(localize('loadMore', "{0} Load More...", icon)); + // })); + // } else { + templateData.historyItemPlaceholderLabel.setLabel(''); + this._loadMoreCallback(element.element.repository); + // } } disposeTemplate(templateData: LoadMoreTemplate): void { @@ -612,78 +511,121 @@ class SCMHistoryTreeKeyboardNavigationLabelProvider implements IKeyboardNavigati type HistoryItemState = { currentHistoryItemGroup: ISCMHistoryItemGroup; items: ISCMHistoryItem[]; loadMore: boolean }; -class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource { +class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource { + + async getChildren(inputOrElement: SCMHistoryViewModel | TreeElement): Promise> { + if (!(inputOrElement instanceof SCMHistoryViewModel)) { + return []; + } + + // History items + const children: TreeElement[] = []; + const historyItems = await inputOrElement.getHistoryItems(); + children.push(...historyItems); + + // Load More element + const repository = inputOrElement.repository.get(); + const lastHistoryItem = tail(historyItems); + if (repository && lastHistoryItem && lastHistoryItem.historyItemViewModel.outputSwimlanes.length > 0) { + children.push({ + repository, + graphColumns: lastHistoryItem.historyItemViewModel.outputSwimlanes, + type: 'historyItemLoadMore' + } satisfies SCMHistoryItemLoadMoreTreeElement); + } + + return children; + } + + hasChildren(inputOrElement: SCMHistoryViewModel | TreeElement): boolean { + return inputOrElement instanceof SCMHistoryViewModel; + } +} + +class SCMHistoryViewModel extends Disposable { + + private readonly _closedRepository = observableFromEvent( + this, + this._scmService.onDidRemoveRepository, + repository => repository); + + private readonly _focusedRepository = observableFromEventOpts( + { owner: this, equalsFn: () => false }, + this._scmViewService.onDidFocusRepository, + () => this._scmViewService.focusedRepository); + + private readonly _activeEditor = observableFromEventOpts( + { owner: this, equalsFn: () => false }, + this._editorService.onDidActiveEditorChange, + () => this._editorService.activeEditor); + + private readonly _activeEditorRepository = derivedObservableWithCache(this, + (reader, lastValue) => { + const activeResource = EditorResourceAccessor.getOriginalUri(this._activeEditor.read(reader)); + if (!activeResource) { + return lastValue; + } + + const repository = this._scmService.getRepository(activeResource); + if (!repository) { + return lastValue; + } + + return Object.create(repository); + }); + + /** + * The focused repository takes precedence over the active editor repository when the observable + * values are updated in the same transaction (or during the initial read of the observable value). + */ + readonly repository = latestChangedValue(this, [this._activeEditorRepository, this._focusedRepository]); + private readonly _state = new Map(); constructor( - @IConfigurationService private readonly _configurationService: IConfigurationService, + @IEditorService private readonly _editorService: IEditorService, + @ISCMService private readonly _scmService: ISCMService, @ISCMViewService private readonly _scmViewService: ISCMViewService ) { super(); - } - async getChildren(inputOrElement: ISCMViewService | TreeElement): Promise> { - const repositoryCount = this._scmViewService.visibleRepositories.length; - const alwaysShowRepositories = this._configurationService.getValue('scm.alwaysShowRepositories') === true; - - if (isSCMViewService(inputOrElement) && (repositoryCount > 1 || alwaysShowRepositories)) { - return this._scmViewService.visibleRepositories; - } else if ((isSCMViewService(inputOrElement) && repositoryCount === 1 && !alwaysShowRepositories) || isSCMRepository(inputOrElement)) { - const children: TreeElement[] = []; - inputOrElement = isSCMRepository(inputOrElement) ? inputOrElement : this._scmViewService.visibleRepositories[0]; - - const historyItems = await this._getHistoryItems(inputOrElement); - children.push(...historyItems); - - const lastHistoryItem = tail(historyItems); - if (lastHistoryItem && lastHistoryItem.historyItemViewModel.outputSwimlanes.length > 0) { - children.push({ - repository: inputOrElement, - graphColumns: lastHistoryItem.historyItemViewModel.outputSwimlanes, - type: 'historyItemLoadMore' - } satisfies SCMHistoryItemLoadMoreTreeElement); + // Closed repository cleanup + this._register(autorun(reader => { + const repository = this._closedRepository.read(reader); + if (!repository) { + return; } - return children; - } - return []; + this._state.delete(repository); + })); } - hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean { - if (isSCMViewService(inputOrElement)) { - return this._scmViewService.visibleRepositories.length !== 0; - } else if (isSCMRepository(inputOrElement)) { - return true; - } else if (isSCMHistoryItemViewModelTreeElement(inputOrElement)) { - return false; - } else if (isSCMHistoryItemLoadMoreTreeElement(inputOrElement)) { - return false; - } else { - throw new Error('hasChildren not implemented.'); - } - } - - clearState(repository?: ISCMRepository): void { + clearRepositoryState(): void { + const repository = this.repository.get(); if (!repository) { - this._state.clear(); return; } this._state.delete(repository); } - loadMore(repository: ISCMRepository): void { + setLoadMore(repository: ISCMRepository, loadMore: boolean): void { const state = this._state.get(repository); if (!state) { return; } - this._state.set(repository, { ...state, loadMore: true }); + this._state.set(repository, { ...state, loadMore }); } - private async _getHistoryItems(element: ISCMRepository): Promise { - let state = this._state.get(element); - const historyProvider = element.provider.historyProvider.get(); + async getHistoryItems(): Promise { + const repository = this.repository.get(); + if (!repository) { + return []; + } + + let state = this._state.get(repository); + const historyProvider = repository.provider.historyProvider.get(); const currentHistoryItemGroup = state?.currentHistoryItemGroup ?? historyProvider?.currentHistoryItemGroup.get(); if (!historyProvider || !currentHistoryItemGroup) { @@ -708,7 +650,7 @@ class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource ({ - repository: element, + repository, historyItemViewModel, type: 'historyItem2' }) satisfies SCMHistoryItemViewModelTreeElement); @@ -739,20 +681,19 @@ class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource; + private _tree!: WorkbenchAsyncDataTree; + private _treeViewModel!: SCMHistoryViewModel; private _treeDataSource!: SCMHistoryTreeDataSource; private _treeIdentityProvider!: SCMHistoryTreeIdentityProvider; - private _repositoryDescription = new Map>(); - private _repositoryLoadMore = new Map>(); + private _repositoryLoadMore = observableValue(this, false); private readonly _actionRunner: IActionRunner; - private readonly _repositories = new DisposableMap(); private readonly _visibilityDisposables = new DisposableStore(); private readonly _treeOperationSequencer = new Sequencer(); private readonly _updateChildrenThrottler = new Throttler(); - private readonly _scmHistoryItemGroupHasRemoteContextKey: IContextKey; + private readonly _scmProviderCtx: IContextKey; private readonly _providerCountBadgeConfig = observableConfigValue<'hidden' | 'auto' | 'visible'>('scm.providerCountBadge', 'hidden', this.configurationService); @@ -774,7 +715,7 @@ export class SCMHistoryViewPane extends ViewPane { ) { super({ ...options, titleMenuId: MenuId.SCMHistoryTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); - this._scmHistoryItemGroupHasRemoteContextKey = this.scopedContextKeyService.createKey('scmHistoryItemGroupHasRemote', undefined); + this._scmProviderCtx = ContextKeys.SCMProvider.bindTo(this.scopedContextKeyService); this._actionRunner = this.instantiationService.createInstance(SCMHistoryViewPaneActionRunner); this._register(this._actionRunner); @@ -804,28 +745,59 @@ export class SCMHistoryViewPane extends ViewPane { this.onDidChangeBodyVisibility(visible => { if (visible) { + this._treeViewModel = this.instantiationService.createInstance(SCMHistoryViewModel); + this._visibilityDisposables.add(this._treeViewModel); + this._treeOperationSequencer.queue(async () => { - await this._tree.setInput(this._scmViewService); - - Event.filter(this.configurationService.onDidChangeConfiguration, - e => - e.affectsConfiguration('scm.alwaysShowRepositories'), - this._visibilityDisposables) - (() => { - this.updateActions(); - this.refresh(); - }, this, this._visibilityDisposables); - - // Add visible repositories - this._scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this._visibilityDisposables); - this._onDidChangeVisibleRepositories({ added: this._scmViewService.visibleRepositories, removed: Iterable.empty() }); - + await this._tree.setInput(this._treeViewModel); this._tree.scrollTop = 0; }); + + this._visibilityDisposables.add(autorunWithStore((reader, store) => { + const repository = this._treeViewModel.repository.read(reader); + if (!repository) { + return; + } + + store.add(autorun(reader => { + const historyProvider = repository.provider.historyProvider.read(reader); + const currentHistoryItemGroupId = historyProvider?.currentHistoryItemGroupId.read(reader); + const currentHistoryItemGroupRevision = historyProvider?.currentHistoryItemGroupRevision.read(reader); + const currentHistoryItemGroupRemoteId = historyProvider?.currentHistoryItemGroupRemoteId.read(reader); + + if (!currentHistoryItemGroupId && !currentHistoryItemGroupRevision && !currentHistoryItemGroupRemoteId) { + return; + } + + this.refresh(); + })); + + store.add(autorun(reader => { + const historyProvider = repository.provider.historyProvider.read(reader); + const currentHistoryItemGroupRemoteRevision = historyProvider?.currentHistoryItemGroupRemoteRevision.read(reader); + + if (!currentHistoryItemGroupRemoteRevision) { + return; + } + + // Remote revision changes can occur as a result of a user action (Fetch, Push) but + // it can also occur as a result of background action (Auto Fetch). If the tree is + // scrolled to the top, we can safely refresh the tree. + if (this._tree.scrollTop === 0) { + this.refresh(); + return; + } + + // Set the "OUTDATED" description + this.updateTitleDescription(`${this.titleDescription} (${localize('outdated', "OUTDATED")})`); + })); + + this._updateChildren(); + this.updateTitleDescription(repository.provider.name); + this._scmProviderCtx.set(repository.provider.contextValue); + })); } else { - this._treeDataSource.clearState(); this._visibilityDisposables.clear(); - this._repositories.clearAndDisposeAll(); } }); } @@ -834,11 +806,14 @@ export class SCMHistoryViewPane extends ViewPane { return this._actionRunner; } - async refresh(repository?: ISCMRepository): Promise { - this._treeDataSource.clearState(repository); - await this._updateChildren(repository); + override getActionsContext(): ISCMProvider | undefined { + return this._treeViewModel?.repository.get()?.provider; + } - this._setRepositoryDescription(repository, ''); + async refresh(): Promise { + await this._updateChildren(true); + + this.updateTitleDescription(this._treeViewModel.repository.get()?.provider.name); this._tree.scrollTop = 0; } @@ -857,20 +832,22 @@ export class SCMHistoryViewPane extends ViewPane { container, new ListDelegate(), [ - this.instantiationService.createInstance(RepositoryRenderer, repository => this._getRepositoryDescription(repository), this._actionRunner, getActionViewItemProvider(this.instantiationService)), this.instantiationService.createInstance(HistoryItemRenderer, historyItemHoverDelegate), - this.instantiationService.createInstance(HistoryItemLoadMoreRenderer, repository => this._getLoadMore(repository), repository => this._loadMoreCallback(repository)), + this.instantiationService.createInstance( + HistoryItemLoadMoreRenderer, + // () => this._repositoryLoadMore, + repository => this._loadMoreCallback(repository)), ], this._treeDataSource, { accessibilityProvider: new SCMHistoryTreeAccessibilityProvider(), identityProvider: this._treeIdentityProvider, - collapseByDefault: (e: unknown) => !isSCMRepository(e), + collapseByDefault: (e: unknown) => false, keyboardNavigationLabelProvider: new SCMHistoryTreeKeyboardNavigationLabelProvider(), horizontalScrolling: false, multipleSelectionSupport: false, } - ) as WorkbenchAsyncDataTree; + ) as WorkbenchAsyncDataTree; this._register(this._tree); this._tree.onDidOpen(this._onDidOpen, this, this._store); @@ -880,8 +857,6 @@ export class SCMHistoryViewPane extends ViewPane { private async _onDidOpen(e: IOpenEvent): Promise { if (!e.element) { return; - } else if (isSCMRepository(e.element)) { - this._scmViewService.focus(e.element); } else if (isSCMHistoryItemViewModelTreeElement(e.element)) { const historyItem = e.element.historyItemViewModel.historyItem; const historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : undefined; @@ -897,8 +872,6 @@ export class SCMHistoryViewPane extends ViewPane { await this._commandService.executeCommand('_workbench.openMultiDiffEditor', { title, multiDiffSourceUri, resources: historyItemChanges }); } - - this._scmViewService.focus(e.element.repository); } else if (isSCMHistoryItemLoadMoreTreeElement(e.element)) { const repositoryCount = this._scmViewService.visibleRepositories.length; const alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories') === true; @@ -913,27 +886,16 @@ export class SCMHistoryViewPane extends ViewPane { private _onContextMenu(e: ITreeContextMenuEvent): void { const element = e.element; - if (!element) { + if (!element || !isSCMHistoryItemViewModelTreeElement(element)) { return; } - let actions: IAction[] = []; - let context: TreeElement | ISCMProvider = element; - let actionRunner: IActionRunner = new HistoryItemActionRunner(() => this._getSelectedHistoryItems()); + const context: TreeElement = element; + const actionRunner: IActionRunner = new HistoryItemActionRunner(() => this._getSelectedHistoryItems()); - if (isSCMRepository(element)) { - const menus = this._scmViewService.menus.getRepositoryMenus(element.provider); - const menu = menus.repositoryContextMenu; - - actions = collectContextMenuActions(menu); - actionRunner = new RepositoryActionRunner(() => this._getSelectedRepositories()); - context = element.provider; - } else if (isSCMHistoryItemViewModelTreeElement(element)) { - const menus = this._scmViewService.menus.getRepositoryMenus(element.repository.provider); - const menu = menus.historyProviderMenu?.getHistoryItemMenu2(element); - - actions = menu ? collectContextMenuActions(menu) : []; - } + const menus = this._scmViewService.menus.getRepositoryMenus(element.repository.provider); + const menu = menus.historyProviderMenu?.getHistoryItemMenu2(element); + const actions = menu ? collectContextMenuActions(menu) : []; actionRunner.onWillRun(() => this._tree.domFocus()); @@ -945,150 +907,42 @@ export class SCMHistoryViewPane extends ViewPane { }); } - private _onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void { - // Added repositories - for (const repository of added) { - const repositoryDisposables = new DisposableStore(); - - repositoryDisposables.add(autorun(reader => { - const historyProvider = repository.provider.historyProvider.read(reader); - const currentHistoryItemGroupId = historyProvider?.currentHistoryItemGroupId.read(reader); - const currentHistoryItemGroupRevision = historyProvider?.currentHistoryItemGroupRevision.read(reader); - const currentHistoryItemGroupRemoteId = historyProvider?.currentHistoryItemGroupRemoteId.read(reader); - - // Update scmHistoryItemGroupHasRemote context key - if (this._scmViewService.visibleRepositories.length === 1) { - this._scmHistoryItemGroupHasRemoteContextKey.set(!!currentHistoryItemGroupRemoteId); - } else { - this._scmHistoryItemGroupHasRemoteContextKey.reset(); - } - - if (!currentHistoryItemGroupId && !currentHistoryItemGroupRevision && !currentHistoryItemGroupRemoteId) { - return; - } - - this.refresh(repository); - })); - - repositoryDisposables.add(autorun(reader => { - const historyProvider = repository.provider.historyProvider.read(reader); - const currentHistoryItemGroupRemoteRevision = historyProvider?.currentHistoryItemGroupRemoteRevision.read(reader); - - if (!currentHistoryItemGroupRemoteRevision) { - return; - } - - // Remote revision changes can occur as a result of a user action (Fetch, Push) but - // it can also occur as a result of background action (Auto Fetch). If the tree is - // scrolled to the top, we can safely refresh the tree. - if (this._tree.scrollTop === 0) { - this.refresh(repository); - return; - } - - // Set the "OUTDATED" description - const description = localize('outdated', "OUTDATED"); - this._setRepositoryDescription(this._isRepositoryNodeVisible() ? repository : undefined, description); - })); - - this._repositories.set(repository, repositoryDisposables); - } - - // Removed repositories - for (const repository of removed) { - this._treeDataSource.clearState(repository); - this._repositoryDescription.delete(repository); - this._repositoryLoadMore.delete(repository); - this._repositories.deleteAndDispose(repository); - } - - this._updateChildren(); - } - - private _getSelectedRepositories(): ISCMRepository[] { - const focusedRepositories = this._tree.getFocus().filter(r => !!r && isSCMRepository(r))! as ISCMRepository[]; - const selectedRepositories = this._tree.getSelection().filter(r => !!r && isSCMRepository(r))! as ISCMRepository[]; - - return Array.from(new Set([...focusedRepositories, ...selectedRepositories])); - } - private _getSelectedHistoryItems(): SCMHistoryItemViewModelTreeElement[] { return this._tree.getSelection() .filter(r => !!r && isSCMHistoryItemViewModelTreeElement(r))!; } - private _getLoadMore(repository: ISCMRepository): ISettableObservable { - let loadMore = this._repositoryLoadMore.get(repository); - if (!loadMore) { - loadMore = observableValue(this, false); - this._repositoryLoadMore.set(repository, loadMore); - } - - return loadMore; - } - private async _loadMoreCallback(repository: ISCMRepository): Promise { - const loadMore = this._getLoadMore(repository); - if (loadMore.get()) { + if (this._repositoryLoadMore.get()) { return; } - loadMore.set(true, undefined); - this._treeDataSource.loadMore(repository); + this._repositoryLoadMore.set(true, undefined); + this._treeViewModel.setLoadMore(repository, true); - await this._updateChildren(repository); - loadMore.set(false, undefined); + await this._updateChildren(); + this._repositoryLoadMore.set(false, undefined); } - private _getRepositoryDescription(repository: ISCMRepository): ISettableObservable { - let description = this._repositoryDescription.get(repository); - if (!description) { - description = observableValue(this, ''); - this._repositoryDescription.set(repository, description); - } - - return description; - } - - private _setRepositoryDescription(repository: ISCMRepository | undefined, description: string): void { - if (!repository) { - this.updateTitleDescription(description); - } else { - this._getRepositoryDescription(repository).set(description, undefined); - } - } - - private _isRepositoryNodeVisible(): boolean { - const repositoryCount = this._scmViewService.visibleRepositories.length; - const alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories') === true; - - return alwaysShowRepositories || repositoryCount > 1; - } - - private _updateChildren(element?: ISCMRepository): Promise { + private _updateChildren(clearCache = false): Promise { return this._updateChildrenThrottler.queue( () => this._treeOperationSequencer.queue( async () => { + if (clearCache) { + this._treeViewModel.clearRepositoryState(); + } + await this._progressService.withProgress({ location: this.id }, async () => { - if (element && this._tree.hasNode(element)) { - // Refresh specific repository - await this._tree.updateChildren(element, undefined, undefined, { - // diffIdentityProvider: this._treeIdentityProvider - }); - } else { - // Refresh the entire tree - await this._tree.updateChildren(undefined, undefined, undefined, { - // diffIdentityProvider: this._treeIdentityProvider - }); - } + await this._tree.updateChildren(undefined, undefined, undefined, { + // diffIdentityProvider: this._treeIdentityProvider + }); }); })); } override dispose(): void { this._visibilityDisposables.dispose(); - this._repositories.dispose(); super.dispose(); } } diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 37ba9728647..5bd33ba943f 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -19,10 +19,6 @@ import { reset } from '../../../../base/browser/dom.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IResourceNode, ResourceTree } from '../../../../base/common/resourceTree.js'; -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); }