diff --git a/src/vs/sessions/contrib/chat/browser/syncIndicator.ts b/src/vs/sessions/contrib/chat/browser/syncIndicator.ts index e07089daee537..b63411982720a 100644 --- a/src/vs/sessions/contrib/chat/browser/syncIndicator.ts +++ b/src/vs/sessions/contrib/chat/browser/syncIndicator.ts @@ -7,6 +7,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { autorun } from '../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -24,6 +25,7 @@ export class SyncIndicator extends Disposable { private _repository: IGitRepository | undefined; private _selectedBranch: string | undefined; private _visible = true; + private _syncing = false; private readonly _renderDisposables = this._register(new DisposableStore()); private readonly _stateDisposables = this._register(new DisposableStore()); @@ -81,13 +83,13 @@ export class SyncIndicator extends Disposable { this._renderDisposables.add(dom.addDisposableListener(button, dom.EventType.CLICK, (e) => { dom.EventHelper.stop(e, true); - this.commandService.executeCommand(GIT_SYNC_COMMAND, this._repository?.rootUri); + this._executeSyncCommand(); })); this._renderDisposables.add(dom.addDisposableListener(button, dom.EventType.KEY_DOWN, (e) => { if (e.key === 'Enter' || e.key === ' ') { dom.EventHelper.stop(e, true); - this.commandService.executeCommand(GIT_SYNC_COMMAND, this._repository?.rootUri); + this._executeSyncCommand(); } })); @@ -102,6 +104,20 @@ export class SyncIndicator extends Disposable { this._update(); } + private async _executeSyncCommand(): Promise { + if (this._syncing) { + return; + } + this._syncing = true; + this._update(); + try { + await this.commandService.executeCommand(GIT_SYNC_COMMAND, this._repository?.rootUri); + } finally { + this._syncing = false; + this._update(); + } + } + private _getAheadBehind(): { ahead: number; behind: number } | undefined { if (!this._repository) { return undefined; @@ -132,7 +148,7 @@ export class SyncIndicator extends Disposable { } const counts = this._getAheadBehind(); - if (!counts || !this._visible) { + if ((!counts && !this._syncing) || !this._visible) { this._slotElement.style.display = 'none'; return; } @@ -140,24 +156,26 @@ export class SyncIndicator extends Disposable { this._slotElement.style.display = ''; dom.clearNode(this._buttonElement); - dom.append(this._buttonElement, renderIcon(Codicon.sync)); + dom.append(this._buttonElement, renderIcon(this._syncing ? ThemeIcon.modify(Codicon.sync, 'spin') : Codicon.sync)); - const parts: string[] = []; - if (counts.behind > 0) { - parts.push(`${counts.behind}↓`); - } - if (counts.ahead > 0) { - parts.push(`${counts.ahead}↑`); - } + if (counts) { + const parts: string[] = []; + if (counts.behind > 0) { + parts.push(`${counts.behind}↓`); + } + if (counts.ahead > 0) { + parts.push(`${counts.ahead}↑`); + } - const label = dom.append(this._buttonElement, dom.$('span.sessions-chat-dropdown-label')); - label.textContent = parts.join('\u00a0'); + const label = dom.append(this._buttonElement, dom.$('span.sessions-chat-dropdown-label')); + label.textContent = parts.join('\u00a0'); + } this._buttonElement.title = localize( 'syncIndicator.tooltip', "Synchronize Changes ({0} to pull, {1} to push)", - counts.behind, - counts.ahead, + counts?.behind ?? 0, + counts?.ahead ?? 0, ); } } diff --git a/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts b/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts index 65869c6cb8b5b..2aed42bfd4cf4 100644 --- a/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts +++ b/src/vs/sessions/contrib/gitSync/browser/gitSync.contribution.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun } from '../../../../base/common/observable.js'; +import { autorun, observableValue } from '../../../../base/common/observable.js'; import { Codicon } from '../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -27,6 +28,7 @@ class GitSyncContribution extends Disposable implements IWorkbenchContribution { private readonly _syncActionDisposable = this._register(new MutableDisposable()); private readonly _gitRepoDisposables = this._register(new DisposableStore()); + private readonly _isSyncing = observableValue(this, false); constructor( @IContextKeyService private readonly contextKeyService: IContextKeyService, @@ -60,6 +62,7 @@ class GitSyncContribution extends Disposable implements IWorkbenchContribution { } repoDisposables.add(autorun(innerReader => { const state = repository.state.read(innerReader); + const isSyncing = this._isSyncing.read(innerReader); const head = state.HEAD; if (!head?.upstream) { this._syncActionDisposable.clear(); @@ -70,14 +73,16 @@ class GitSyncContribution extends Disposable implements IWorkbenchContribution { const behind = head.behind ?? 0; const hasSyncChanges = ahead > 0 || behind > 0; contextKey.set(hasSyncChanges); - this._syncActionDisposable.value = registerSyncAction(behind, ahead); + this._syncActionDisposable.value = registerSyncAction(behind, ahead, isSyncing, (syncing) => { + this._isSyncing.set(syncing, undefined); + }); })); }); })); } } -function registerSyncAction(behind: number, ahead: number): IDisposable { +function registerSyncAction(behind: number, ahead: number, isSyncing: boolean, setSyncing: (syncing: boolean) => void): IDisposable { if (behind === 0 && ahead === 0) { return Disposable.None; } @@ -89,6 +94,8 @@ function registerSyncAction(behind: number, ahead: number): IDisposable { title += `${ahead}↑`; } + const icon = isSyncing ? ThemeIcon.modify(Codicon.sync, 'spin') : Codicon.sync; + class SynchronizeChangesAction extends Action2 { static readonly ID = 'chatEditing.synchronizeChanges'; @@ -97,7 +104,7 @@ function registerSyncAction(behind: number, ahead: number): IDisposable { id: SynchronizeChangesAction.ID, title, tooltip: localize('synchronizeChanges', "Synchronize Changes with Git (Behind {0}, Ahead {1})", behind, ahead), - icon: Codicon.sync, + icon, category: CHAT_CATEGORY, menu: [ { @@ -114,7 +121,12 @@ function registerSyncAction(behind: number, ahead: number): IDisposable { const commandService = accessor.get(ICommandService); const sessionManagementService = accessor.get(ISessionsManagementService); const worktreeUri = sessionManagementService.getActiveSession()?.worktree; - await commandService.executeCommand('git.sync', worktreeUri); + setSyncing(true); + try { + await commandService.executeCommand('git.sync', worktreeUri); + } finally { + setSyncing(false); + } } } return registerAction2(SynchronizeChangesAction);