diff --git a/extensions/git/src/actionButton.ts b/extensions/git/src/actionButton.ts new file mode 100644 index 00000000000..d8b45a00c26 --- /dev/null +++ b/extensions/git/src/actionButton.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, Event, EventEmitter, SourceControlActionButton, Uri, workspace } from 'vscode'; +import * as nls from 'vscode-nls'; +import { Repository, Operation } from './repository'; +import { dispose } from './util'; +import { Branch } from './api/git'; + +const localize = nls.loadMessageBundle(); + +interface ActionButtonState { + readonly HEAD: Branch | undefined; + readonly isSyncRunning: boolean; + readonly repositoryHasNoChanges: boolean; +} + +export class ActionButtonCommand { + private _onDidChange = new EventEmitter(); + get onDidChange(): Event { return this._onDidChange.event; } + + private _state: ActionButtonState; + private get state() { return this._state; } + private set state(state: ActionButtonState) { + if (JSON.stringify(this._state) !== JSON.stringify(state)) { + this._state = state; + this._onDidChange.fire(); + } + } + + private disposables: Disposable[] = []; + + constructor(readonly repository: Repository) { + this._state = { HEAD: undefined, isSyncRunning: false, repositoryHasNoChanges: false }; + + repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables); + repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables); + } + + get button(): SourceControlActionButton | undefined { + if (!this.state.HEAD || !this.state.HEAD.name || !this.state.HEAD.commit) { return undefined; } + + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + const showActionButton = config.get('showUnpublishedCommitsButton', 'whenEmpty'); + const postCommitCommand = config.get('postCommitCommand'); + const noPostCommitCommand = postCommitCommand !== 'sync' && postCommitCommand !== 'push'; + + let actionButton: SourceControlActionButton | undefined; + if (showActionButton === 'always' || (showActionButton === 'whenEmpty' && this.state.repositoryHasNoChanges && noPostCommitCommand)) { + if (this.state.HEAD.upstream) { + if (this.state.HEAD.ahead) { + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + const rebaseWhenSync = config.get('rebaseWhenSync'); + + const ahead = `${this.state.HEAD.ahead}$(arrow-up)`; + const behind = this.state.HEAD.behind ? `${this.state.HEAD.behind}$(arrow-down) ` : ''; + const icon = this.state.isSyncRunning ? '$(sync~spin)' : '$(sync)'; + + actionButton = { + command: { + command: this.state.isSyncRunning ? '' : rebaseWhenSync ? 'git.syncRebase' : 'git.sync', + title: localize('scm button sync title', "{0} {1}{2}", icon, behind, ahead), + tooltip: this.state.isSyncRunning ? + localize('syncing changes', "Synchronizing Changes...") + : this.repository.syncTooltip, + arguments: [this.repository.sourceControl], + }, + description: localize('scm button sync description', "{0} Sync Changes {1}{2}", icon, behind, ahead) + }; + } + } else { + actionButton = { + command: { + command: this.state.isSyncRunning ? '' : 'git.publish', + title: localize('scm button publish title', "$(cloud-upload) Publish Branch"), + tooltip: this.state.isSyncRunning ? + localize('scm button publish branch running', "Publishing Branch...") : + localize('scm button publish branch', "Publish Branch"), + arguments: [this.repository.sourceControl], + } + }; + } + } + + return actionButton; + } + + private onDidChangeOperations(): void { + const isSyncRunning = this.repository.operations.isRunning(Operation.Sync) || + this.repository.operations.isRunning(Operation.Push) || + this.repository.operations.isRunning(Operation.Pull); + + this.state = { ...this.state, isSyncRunning }; + } + + private onDidRunGitStatus(): void { + this.state = { + ...this.state, + HEAD: this.repository.HEAD, + repositoryHasNoChanges: + this.repository.indexGroup.resourceStates.length === 0 && + this.repository.mergeGroup.resourceStates.length === 0 && + this.repository.untrackedGroup.resourceStates.length === 0 && + this.repository.workingTreeGroup.resourceStates.length === 0 + }; + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 3d9aa32a46c..d409a337ffa 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, SourceControlActionButton } from 'vscode'; +import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery, FetchOptions } from './api/git'; @@ -20,6 +20,7 @@ import { Log, LogLevel } from './log'; import { IPushErrorHandlerRegistry } from './pushError'; import { ApiRepository } from './api/api1'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; +import { ActionButtonCommand } from './actionButton'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -966,6 +967,11 @@ export class Repository implements Disposable { statusBar.onDidChange(() => this._sourceControl.statusBarCommands = statusBar.commands, null, this.disposables); this._sourceControl.statusBarCommands = statusBar.commands; + const actionButton = new ActionButtonCommand(this); + this.disposables.push(actionButton); + actionButton.onDidChange(() => this._sourceControl.actionButton = actionButton.button); + this._sourceControl.actionButton = actionButton.button; + const progressManager = new ProgressManager(this); this.disposables.push(progressManager); @@ -1935,43 +1941,6 @@ export class Repository implements Disposable { return undefined; }); - let actionButton: SourceControlActionButton | undefined; - if (HEAD !== undefined) { - const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); - const showActionButton = config.get('showUnpublishedCommitsButton', 'whenEmpty'); - const postCommitCommand = config.get('postCommitCommand'); - - if (showActionButton === 'always' || (showActionButton === 'whenEmpty' && workingTree.length === 0 && index.length === 0 && untracked.length === 0 && merge.length === 0 && postCommitCommand !== 'sync' && postCommitCommand !== 'push')) { - if (HEAD.name && HEAD.commit) { - if (HEAD.upstream) { - if (HEAD.ahead) { - const rebaseWhenSync = config.get('rebaseWhenSync'); - - actionButton = { - command: { - command: rebaseWhenSync ? 'git.syncRebase' : 'git.sync', - title: localize('scm button sync title', "$(sync) {0}{1}", HEAD.behind ? `${HEAD.behind}$(arrow-down) ` : '', `${HEAD.ahead}$(arrow-up)`), - tooltip: this.syncTooltip, - arguments: [this._sourceControl], - }, - description: localize('scm button sync description', "$(sync) Sync Changes {0}{1}", HEAD.behind ? `${HEAD.behind}$(arrow-down) ` : '', `${HEAD.ahead}$(arrow-up)`) - }; - } - } else { - actionButton = { - command: { - command: 'git.publish', - title: localize('scm button publish title', "$(cloud-upload) Publish Branch"), - tooltip: localize('scm button publish tooltip', "Publish Branch"), - arguments: [this._sourceControl], - } - }; - } - } - } - } - this._sourceControl.actionButton = actionButton; - // set resource groups this.mergeGroup.resourceStates = merge; this.indexGroup.resourceStates = index; diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 9e09f00c3b6..4895a896cd7 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -2503,7 +2503,7 @@ export class SCMActionButton implements IDisposable { if (button.description) { // ButtonWithDescription - this.button = new ButtonWithDescription(this.container, { supportIcons: true }); + this.button = new ButtonWithDescription(this.container, { supportIcons: true, title: button.command.tooltip }); (this.button as ButtonWithDescription).description = button.description; } else { // Button