diff --git a/extensions/git/package.json b/extensions/git/package.json index b48d7f97fad..b572d9edf68 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2407,20 +2407,29 @@ "description": "%config.timeline.showUncommitted%", "scope": "window" }, - "git.showUnpublishedCommitsButton": { - "type": "string", - "enum": [ - "always", - "whenEmpty", - "never" - ], - "enumDescriptions": [ - "%config.showUnpublishedCommitsButton.always%", - "%config.showUnpublishedCommitsButton.whenEmpty%", - "%config.showUnpublishedCommitsButton.never%" - ], - "default": "whenEmpty", - "description": "%config.showUnpublishedCommitsButton%", + "git.showActionButton": { + "type": "object", + "additionalProperties": false, + "description": "%config.showActionButton%", + "properties": { + "commit": { + "type": "boolean", + "description": "%config.showActionButton.commit%" + }, + "publish": { + "type": "boolean", + "description": "%config.showActionButton.publish%" + }, + "sync": { + "type": "boolean", + "description": "%config.showActionButton.sync%" + } + }, + "default": { + "commit": true, + "publish": true, + "sync": true + }, "scope": "resource" }, "git.statusLimit": { diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index d17b31b71be..40b832ef200 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -219,10 +219,10 @@ "config.timeline.date.committed": "Use the committed date", "config.timeline.date.authored": "Use the authored date", "config.useCommitInputAsStashMessage": "Controls whether to use the message from the commit input box as the default stash message.", - "config.showUnpublishedCommitsButton": "Controls whether to show an action button to sync or publish, if there are unpublished commits.", - "config.showUnpublishedCommitsButton.always": "Always shows the action button, if there are unpublished commits.", - "config.showUnpublishedCommitsButton.whenEmpty": "Only shows the action button if there are no other changes and there are unpublished commits.", - "config.showUnpublishedCommitsButton.never": "Never shows the action button.", + "config.showActionButton": "Controls whether an action button can be shown in the Source Control view.", + "config.showActionButton.commit": "Show an action button to commit changes.", + "config.showActionButton.publish": "Show an action button to publish a local branch.", + "config.showActionButton.sync": "Show an action button to sync changes.", "config.statusLimit": "Controls how to limit the number of changes that can be parsed from Git status command. Can be set to 0 for no limit.", "config.experimental.installGuide": "Experimental improvements for the git setup flow.", "config.repositoryScanIgnoredFolders": "List of folders that are ignored while scanning for Git repositories when `#git.autoRepositoryDetection#` is set to `true` or `subFolders`.", diff --git a/extensions/git/src/actionButton.ts b/extensions/git/src/actionButton.ts index 13efe1d5b89..f461a92ec6b 100644 --- a/extensions/git/src/actionButton.ts +++ b/extensions/git/src/actionButton.ts @@ -37,18 +37,22 @@ export class ActionButtonCommand { repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables); repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables); + + const root = Uri.file(repository.root); + this.disposables.push(workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('git.postCommitCommand', root) || + e.affectsConfiguration('git.showActionButton', root) + ) { + this._onDidChange.fire(); + } + })); } 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.repositoryHasNoChanges) { if (this.state.HEAD.upstream) { // Sync Changes actionButton = this.getSyncChangesActionButton(); @@ -56,28 +60,105 @@ export class ActionButtonCommand { // Publish Branch actionButton = this.getPublishBranchActionButton(); } + } else { + // Commit Changes + actionButton = this.getCommitActionButton(); } return actionButton; } - private getPublishBranchActionButton(): SourceControlActionButton { - return { - command: { - command: 'git.publish', - title: localize('scm publish branch action button title', "{0} Publish Branch", '$(cloud-upload)'), - tooltip: this.state.isActionRunning ? - localize('scm button publish branch running', "Publishing Branch...") : - localize('scm button publish branch', "Publish Branch"), - arguments: [this.repository.sourceControl], - }, - enabled: !this.state.isActionRunning - }; + private getCommitActionButton(): SourceControlActionButton | undefined { + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + const showActionButton = config.get<{ commit: boolean }>('showActionButton', { commit: true }); + + if (showActionButton.commit) { + let title: string, tooltip: string; + const postCommitCommand = config.get('postCommitCommand'); + + switch (postCommitCommand) { + case 'push': { + title = localize('scm button commit and push title', "$(arrow-up) Commit & Push"); + tooltip = this.state.isActionRunning ? + localize('scm button committing pushing tooltip', "Committing & Pushing Changes...") : + localize('scm button commit push tooltip', "Commit & Push Changes"); + break; + } + case 'sync': { + title = localize('scm button commit and sync title', "$(sync) Commit & Sync"); + tooltip = this.state.isActionRunning ? + localize('scm button committing synching tooltip', "Committing & Synching Changes...") : + localize('scm button commit sync tooltip', "Commit & Sync Changes"); + break; + } + default: { + title = localize('scm button commit title', "$(check) Commit"); + tooltip = this.state.isActionRunning ? + localize('scm button committing tooltip', "Committing Changes...") : + localize('scm button commit tooltip', "Commit Changes"); + break; + } + } + + return { + command: { + command: 'git.commit', + title: title, + tooltip: tooltip, + arguments: [this.repository.sourceControl], + }, + secondaryCommands: [ + [ + { + command: 'git.commit', + title: 'Commit', + arguments: [this.repository.sourceControl, ''], + }, + { + command: 'git.commit', + title: 'Commit & Push', + arguments: [this.repository.sourceControl, 'push'], + }, + { + command: 'git.commit', + title: 'Commit & Sync', + arguments: [this.repository.sourceControl, 'sync'], + }, + ] + ], + enabled: !this.state.isActionRunning + }; + } + + return undefined; + } + + private getPublishBranchActionButton(): SourceControlActionButton | undefined { + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + const showActionButton = config.get<{ publish: boolean }>('showActionButton', { publish: true }); + + if (showActionButton.publish) { + return { + command: { + command: 'git.publish', + title: localize('scm publish branch action button title', "{0} Publish Branch", '$(cloud-upload)'), + tooltip: this.state.isActionRunning ? + localize('scm button publish branch running', "Publishing Branch...") : + localize('scm button publish branch', "Publish Branch"), + arguments: [this.repository.sourceControl], + }, + enabled: !this.state.isActionRunning + }; + } + + return undefined; } private getSyncChangesActionButton(): SourceControlActionButton | undefined { - if (this.state.HEAD?.ahead) { - const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + const config = workspace.getConfiguration('git', Uri.file(this.repository.root)); + const showActionButton = config.get<{ sync: boolean }>('showActionButton', { sync: true }); + + if (this.state.HEAD?.ahead && showActionButton.sync) { const rebaseWhenSync = config.get('rebaseWhenSync'); const ahead = `${this.state.HEAD.ahead}$(arrow-up)`; @@ -102,11 +183,13 @@ export class ActionButtonCommand { } private onDidChangeOperations(): void { - const isActionRunning = this.repository.operations.isRunning(Operation.Sync) || + const isActionRunning = + this.repository.operations.isRunning(Operation.Sync) || this.repository.operations.isRunning(Operation.Push) || - this.repository.operations.isRunning(Operation.Pull); + this.repository.operations.isRunning(Operation.Pull) || + this.repository.operations.isRunning(Operation.Commit); - this.state = { ...this.state, isActionRunning: isActionRunning }; + this.state = { ...this.state, isActionRunning }; } private onDidRunGitStatus(): void { diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 14c7447e3e8..6dfba24823e 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -129,6 +129,8 @@ export interface LogOptions { readonly path?: string; } +export type PostCommitCommand = 'push' | 'sync' | string; + export interface CommitOptions { all?: boolean | 'tracked'; amend?: boolean; @@ -139,6 +141,7 @@ export interface CommitOptions { requireUserConfig?: boolean; useEditor?: boolean; verbose?: boolean; + postCommitCommand?: PostCommitCommand; } export interface FetchOptions { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index c46aaa13c57..43498619153 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -9,7 +9,7 @@ import { Command, commands, Disposable, LineChange, MessageOptions, Position, Pr import TelemetryReporter from '@vscode/extension-telemetry'; import * as nls from 'vscode-nls'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; -import { Branch, ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher } from './api/git'; +import { Branch, ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher, PostCommitCommand } from './api/git'; import { Git, Stash } from './git'; import { Model } from './model'; import { Repository, Resource, ResourceGroupType } from './repository'; @@ -1614,14 +1614,11 @@ export class CommandCenter { await repository.commit(message, opts); const postCommitCommand = config.get<'none' | 'push' | 'sync'>('postCommitCommand'); - - switch (postCommitCommand) { - case 'push': - await this._push(repository, { pushType: PushType.Push, silent: true }); - break; - case 'sync': - await this.sync(repository); - break; + if ((opts.postCommitCommand === undefined && postCommitCommand === 'push') || opts.postCommitCommand === 'push') { + await this._push(repository, { pushType: PushType.Push, silent: true }); + } + if ((opts.postCommitCommand === undefined && postCommitCommand === 'sync') || opts.postCommitCommand === 'sync') { + await this.sync(repository); } return true; @@ -1670,8 +1667,8 @@ export class CommandCenter { } @command('git.commit', { repository: true }) - async commit(repository: Repository): Promise { - await this.commitWithAnyInput(repository); + async commit(repository: Repository, postCommitCommand?: PostCommitCommand): Promise { + await this.commitWithAnyInput(repository, { postCommitCommand }); } @command('git.commitStaged', { repository: true }) diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index e6166c8ca12..dd5d72e890c 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -947,7 +947,7 @@ export class Repository implements Disposable { || e.affectsConfiguration('git.ignoreSubmodules', root) || e.affectsConfiguration('git.openDiffOnClick', root) || e.affectsConfiguration('git.rebaseWhenSync', root) - || e.affectsConfiguration('git.showUnpublishedCommitsButton', root) + || e.affectsConfiguration('git.showActionButton', root) )(this.updateModelState, this, this.disposables); const updateInputBoxVisibility = () => { diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 721059b4a27..a2349f9dc96 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -238,6 +238,7 @@ export interface IButtonWithDropdownOptions extends IButtonOptions { readonly contextMenuProvider: IContextMenuProvider; readonly actions: IAction[]; readonly actionRunner?: IActionRunner; + readonly addPrimaryActionToDropdown?: boolean; } export class ButtonWithDropdown extends Disposable implements IButton { @@ -267,7 +268,7 @@ export class ButtonWithDropdown extends Disposable implements IButton { this._register(this.dropdownButton.onDidClick(e => { options.contextMenuProvider.showContextMenu({ getAnchor: () => this.dropdownButton.element, - getActions: () => [this.action, ...options.actions], + getActions: () => options.addPrimaryActionToDropdown === false ? [...options.actions] : [this.action, ...options.actions], actionRunner: options.actionRunner, onHide: () => this.dropdownButton.element.setAttribute('aria-expanded', 'false') }); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 9bb74174799..5af7bb15f92 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1143,6 +1143,7 @@ export interface SCMProviderFeatures { export interface SCMActionButtonDto { command: ICommandDto; + secondaryCommands?: ICommandDto[][]; description?: string; enabled: boolean; } diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index b67e7580f48..ee9ec8daf69 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -537,6 +537,9 @@ class ExtHostSourceControl implements vscode.SourceControl { const internal = actionButton !== undefined ? { command: this._commands.converter.toInternal(actionButton.command, this._actionButtonDisposables.value), + secondaryCommands: actionButton.secondaryCommands?.map(commandGroup => { + return commandGroup.map(command => this._commands.converter.toInternal(command, this._actionButtonDisposables.value!)); + }), description: actionButton.description, enabled: actionButton.enabled } : undefined; diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index a3db991bf05..198babcb503 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -241,6 +241,16 @@ margin: 0 0.2em 0 0; } +.scm-view .button-container > .monaco-button-dropdown { + flex-grow: 1; +} + +.scm-view .button-container > .monaco-button-dropdown > .monaco-dropdown-button { + display:flex; + align-items: center; + padding: 0 4px; +} + .scm-view .scm-editor.hidden { display: none; } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 603015736de..df9e433c2fa 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -20,7 +20,7 @@ import { IContextKeyService, IContextKey, ContextKeyExpr, RawContextKey } from ' import { ICommandService } from 'vs/platform/commands/common/commands'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, MenuRegistry, Action2 } from 'vs/platform/actions/common/actions'; -import { IAction, ActionRunner } from 'vs/base/common/actions'; +import { IAction, ActionRunner, Action, Separator } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, registerThemingParticipant, IFileIconTheme, ThemeIcon, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton } from './util'; @@ -82,7 +82,7 @@ import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/ import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; -import { Button, ButtonWithDescription } from 'vs/base/browser/ui/button/button'; +import { Button, ButtonWithDescription, ButtonWithDropdown } from 'vs/base/browser/ui/button/button'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { RepositoryContextKeys } from 'vs/workbench/contrib/scm/browser/scmViewService'; import { DropIntoEditorController } from 'vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution'; @@ -110,6 +110,7 @@ class ActionButtonRenderer implements ICompressibleTreeRenderer { }); export class SCMActionButton implements IDisposable { - private button: Button | ButtonWithDescription | undefined; + private button: Button | ButtonWithDescription | ButtonWithDropdown | undefined; private readonly disposables = new MutableDisposable(); constructor( private readonly container: HTMLElement, + private readonly contextMenuService: IContextMenuService, private readonly commandService: ICommandService, private readonly themeService: IThemeService, private readonly notificationService: INotificationService @@ -2617,7 +2619,31 @@ export class SCMActionButton implements IDisposable { return; } - if (button.description) { + const executeButtonAction = async (commandId: string, ...args: any[]) => { + try { + await this.commandService.executeCommand(commandId, ...args); + } catch (ex) { + this.notificationService.error(ex); + } + }; + + if (button.secondaryCommands?.length) { + const actions: IAction[] = []; + for (const commandGroup of button.secondaryCommands) { + for (const command of commandGroup) { + actions.push(new Action(command.id, command.title, undefined, true, async () => await executeButtonAction(command.id, ...(command.arguments || [])))); + } + actions.push(new Separator()); + } + + // ButtonWithDropdown + this.button = new ButtonWithDropdown(this.container, { + actions: actions, + addPrimaryActionToDropdown: false, + contextMenuProvider: this.contextMenuService, + supportIcons: true + }); + } else if (button.description) { // ButtonWithDescription this.button = new ButtonWithDescription(this.container, { supportIcons: true, title: button.command.tooltip }); (this.button as ButtonWithDescription).description = button.description; @@ -2628,13 +2654,8 @@ export class SCMActionButton implements IDisposable { this.button.enabled = button.enabled; this.button.label = button.command.title; - this.button.onDidClick(async () => { - try { - await this.commandService.executeCommand(button.command.id, ...(button.command.arguments || [])); - } catch (ex) { - this.notificationService.error(ex); - } - }, null, this.disposables.value); + this.button.element.title = button.command.tooltip ?? ''; + this.button.onDidClick(async () => await executeButtonAction(button.command.id, ...(button.command.arguments || [])), null, this.disposables.value); this.disposables.value!.add(this.button); this.disposables.value!.add(attachButtonStyler(this.button, this.themeService)); diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index f1d5142b48c..0629e491de5 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -99,6 +99,7 @@ export interface ISCMInputChangeEvent { export interface ISCMActionButtonDescriptor { command: Command; + secondaryCommands?: Command[][]; description?: string; enabled: boolean; } diff --git a/src/vscode-dts/vscode.proposed.scmActionButton.d.ts b/src/vscode-dts/vscode.proposed.scmActionButton.d.ts index 2934db8d3bc..b035aaddc8d 100644 --- a/src/vscode-dts/vscode.proposed.scmActionButton.d.ts +++ b/src/vscode-dts/vscode.proposed.scmActionButton.d.ts @@ -8,6 +8,7 @@ declare module 'vscode' { export interface SourceControlActionButton { command: Command; + secondaryCommands?: Command[][]; description?: string; enabled: boolean; }