Adds scm action button

Refs: #110882
This commit is contained in:
Eric Amodio
2021-09-27 01:16:09 -04:00
parent 53c5d3cb4a
commit 55c726b53e
10 changed files with 214 additions and 13 deletions
+27
View File
@@ -1906,6 +1906,33 @@ export class Repository implements Disposable {
return undefined;
});
let actionButton: SourceControl['actionButton'];
if (HEAD !== undefined && workingTree.length === 0 && index.length === 0 && untracked.length === 0 && merge.length === 0) {
if (HEAD.name && HEAD.commit) {
if (HEAD.upstream) {
if (HEAD.ahead) {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const rebaseWhenSync = config.get<string>('rebaseWhenSync');
actionButton = {
command: rebaseWhenSync ? 'git.syncRebase' : 'git.sync',
title: localize('scm button sync title', ' Sync Changes \u00a0$(sync){0}{1}', HEAD.behind ? `${HEAD.behind}$(arrow-down) ` : '', `${HEAD.ahead}$(arrow-up)`),
tooltip: this.syncTooltip,
arguments: [this._sourceControl],
};
}
} else {
actionButton = {
command: 'git.publish',
title: localize('scm button publish title', "$(cloud-upload) Publish Changes"),
tooltip: localize('scm button publish tooltip', "Publish Changes"),
arguments: [this._sourceControl],
};
}
}
}
this._sourceControl.actionButton = actionButton;
// set resource groups
this.mergeGroup.resourceStates = merge;
this.indexGroup.resourceStates = index;
+4
View File
@@ -2820,4 +2820,8 @@ declare module 'vscode' {
}
//#endregion
export interface SourceControl {
actionButton?: Command;
}
}
@@ -120,6 +120,7 @@ class MainThreadSCMProvider implements ISCMProvider {
get commitTemplate(): string { return this.features.commitTemplate || ''; }
get acceptInputCommand(): Command | undefined { return this.features.acceptInputCommand; }
get actionButton(): Command | undefined { return this.features.actionButton ?? undefined; }
get statusBarCommands(): Command[] | undefined { return this.features.statusBarCommands; }
get count(): number | undefined { return this.features.count; }
@@ -1053,6 +1053,7 @@ export interface SCMProviderFeatures {
count?: number;
commitTemplate?: string;
acceptInputCommand?: modes.Command;
actionButton?: ICommandDto | null;
statusBarCommands?: ICommandDto[];
}
+14
View File
@@ -509,6 +509,19 @@ class ExtHostSourceControl implements vscode.SourceControl {
this._proxy.$updateSourceControl(this.handle, { acceptInputCommand: internal });
}
private _actionButtonDisposables = new MutableDisposable<DisposableStore>();
private _actionButton: vscode.Command | undefined;
get actionButton(): vscode.Command | undefined { return this._actionButton; }
set actionButton(actionButton: vscode.Command | undefined) {
this._actionButtonDisposables.value = new DisposableStore();
this._actionButton = actionButton;
const internal = actionButton !== undefined ? this._commands.converter.toInternal(this._actionButton, this._actionButtonDisposables.value) : undefined;
this._proxy.$updateSourceControl(this.handle, { actionButton: internal ?? null });
}
private _statusBarDisposables = new MutableDisposable<DisposableStore>();
private _statusBarCommands: vscode.Command[] | undefined = undefined;
@@ -630,6 +643,7 @@ class ExtHostSourceControl implements vscode.SourceControl {
dispose(): void {
this._acceptInputDisposables.dispose();
this._actionButtonDisposables.dispose();
this._statusBarDisposables.dispose();
this._groups.forEach(group => group.dispose());
@@ -194,6 +194,17 @@
justify-content: center;
}
.scm-view .button-container {
margin-left: 8px;
height: 100%;
display: flex;
align-items: center;
}
.scm-view .button-container .codicon {
font-size: small !important;
}
.scm-view .scm-editor.hidden {
display: none;
}
@@ -207,6 +207,11 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
type: 'number',
description: localize('providersVisible', "Controls how many repositories are visible in the Source Control Repositories section. Set to `0` to be able to manually resize the view."),
default: 10
},
'scm.showActionButtons': {
type: 'boolean',
markdownDescription: localize('showActionButtons', "Controls whether action buttons are visible in the SCM view."),
default: true
}
}
});
@@ -6,11 +6,11 @@
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 } from 'vs/base/common/lifecycle';
import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
import { append, $, Dimension, asCSSUrl, trackFocus } from 'vs/base/browser/dom';
import { append, $, Dimension, asCSSUrl, trackFocus, clearNode } 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 } from 'vs/workbench/contrib/scm/common/scm';
import { ISCMResourceGroup, ISCMResource, InputValidationType, ISCMRepository, ISCMInput, IInputValidation, ISCMViewService, ISCMViewVisibleRepositoryChangeEvent, ISCMService, SCMInputChangeReason, VIEW_PANE_ID, ISCMActionButton } from 'vs/workbench/contrib/scm/common/scm';
import { ResourceLabels, IResourceLabel } from 'vs/workbench/browser/labels';
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
@@ -23,8 +23,8 @@ import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options,
import { IAction, ActionRunner } 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 } from './util';
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton } from './util';
import { attachBadgeStyler, attachButtonStyler } from 'vs/platform/theme/common/styler';
import { WorkbenchCompressibleObjectTree, 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';
@@ -82,8 +82,11 @@ 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/browser/core/markdownRenderer';
import { Button } from 'vs/base/browser/ui/button/button';
import { Command } from 'vs/editor/common/modes';
import { INotificationService } from 'vs/platform/notification/common/notification';
type TreeElement = ISCMRepository | ISCMInput | ISCMResourceGroup | IResourceNode<ISCMResource, ISCMResourceGroup> | ISCMResource;
type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | IResourceNode<ISCMResource, ISCMResourceGroup> | ISCMResource;
interface ISCMLayout {
height: number | undefined;
@@ -91,6 +94,51 @@ interface ISCMLayout {
readonly onDidChange: Event<void>;
}
interface ActionButtonTemplate {
readonly actionButton: ScmActionButton;
disposable: IDisposable;
readonly templateDisposable: IDisposable;
}
class ActionButtonRenderer implements ICompressibleTreeRenderer<ISCMActionButton, FuzzyScore, ActionButtonTemplate> {
static readonly DEFAULT_HEIGHT = 30;
static readonly TEMPLATE_ID = 'actionButton';
get templateId(): string { return ActionButtonRenderer.TEMPLATE_ID; }
constructor(
@ICommandService private commandService: ICommandService,
@IThemeService private themeService: IThemeService,
@INotificationService private notificationService: INotificationService,
) { }
renderTemplate(container: HTMLElement): ActionButtonTemplate {
const buttonContainer = append(container, $('.button-container'));
const actionButton = new ScmActionButton(buttonContainer, this.commandService, this.themeService, this.notificationService);
return { actionButton, disposable: Disposable.None, templateDisposable: actionButton };
}
renderElement(node: ITreeNode<ISCMActionButton, FuzzyScore>, index: number, templateData: ActionButtonTemplate, height: number | undefined): void {
templateData.disposable.dispose();
templateData.actionButton.setCommand(node.element.button);
}
renderCompressedElements(): void {
throw new Error('Should never happen since node is incompressible');
}
disposeElement(node: ITreeNode<ISCMActionButton, FuzzyScore>, index: number, template: ActionButtonTemplate): void {
template.disposable.dispose();
}
disposeTemplate(templateData: ActionButtonTemplate): void {
templateData.disposable.dispose();
templateData.templateDisposable.dispose();
}
}
interface InputTemplate {
readonly inputWidget: SCMInputWidget;
disposable: IDisposable;
@@ -529,6 +577,8 @@ class ListDelegate implements IListVirtualDelegate<TreeElement> {
getHeight(element: TreeElement) {
if (isSCMInput(element)) {
return this.inputRenderer.getHeight(element);
} else if (isSCMActionButton(element)) {
return ActionButtonRenderer.DEFAULT_HEIGHT + 10;
} else {
return 22;
}
@@ -539,6 +589,8 @@ class ListDelegate implements IListVirtualDelegate<TreeElement> {
return RepositoryRenderer.TEMPLATE_ID;
} else if (isSCMInput(element)) {
return InputRenderer.TEMPLATE_ID;
} else if (isSCMActionButton(element)) {
return ActionButtonRenderer.TEMPLATE_ID;
} else if (ResourceTree.isResourceNode(element) || isSCMResource(element)) {
return ResourceRenderer.TEMPLATE_ID;
} else {
@@ -582,6 +634,12 @@ export class SCMTreeSorter implements ITreeSorter<TreeElement> {
return 1;
}
if (isSCMActionButton(one)) {
return -1;
} else if (isSCMActionButton(other)) {
return 1;
}
if (isSCMResourceGroup(one)) {
if (!isSCMResourceGroup(other)) {
throw new Error('Invalid comparison');
@@ -642,9 +700,7 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb
getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | { toString(): string; }[] | undefined {
if (ResourceTree.isResourceNode(element)) {
return element.name;
} else if (isSCMRepository(element)) {
return undefined;
} else if (isSCMInput(element)) {
} else if (isSCMRepository(element) || isSCMInput(element) || isSCMActionButton(element)) {
return undefined;
} else if (isSCMResourceGroup(element)) {
return element.label;
@@ -682,6 +738,9 @@ function getSCMResourceId(element: TreeElement): string {
} else if (isSCMInput(element)) {
const provider = element.repository.provider;
return `input:${provider.id}`;
} else if (isSCMActionButton(element)) {
const provider = element.repository.provider;
return `actionButton:${provider.id}`;
} else if (isSCMResource(element)) {
const group = element.resourceGroup;
const provider = group.provider;
@@ -727,6 +786,8 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider<Tree
return `${folderName} ${element.provider.label}`;
} else if (isSCMInput(element)) {
return localize('input', "Source Control Input");
} else if (isSCMActionButton(element)) {
return element.button?.title ?? '';
} else if (isSCMResourceGroup(element)) {
return element.label;
} else {
@@ -990,6 +1051,7 @@ class ViewModel {
private visibilityDisposables = new DisposableStore();
private scrollTop: number | undefined;
private alwaysShowRepositories = false;
private showActionButtons = false;
private firstVisible = true;
private disposables = new DisposableStore();
@@ -1033,8 +1095,9 @@ class ViewModel {
}
private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void {
if (!e || e.affectsConfiguration('scm.alwaysShowRepositories')) {
if (!e || e.affectsConfiguration('scm.alwaysShowRepositories') || e.affectsConfiguration('scm.showActionButtons')) {
this.alwaysShowRepositories = this.configurationService.getValue<boolean>('scm.alwaysShowRepositories');
this.showActionButtons = this.configurationService.getValue<boolean>('scm.showActionButtons');
this.refresh();
}
}
@@ -1176,10 +1239,23 @@ class ViewModel {
children.push({ element: item.element.input, incompressible: true, collapsible: false });
}
if (this.items.size === 1 || hasSomeChanges) {
if (hasSomeChanges || (this.items.size === 1 && (!this.showActionButtons || !item.element.provider.actionButton))) {
children.push(...item.groupItems.map(i => this.render(i, treeViewState)));
}
if (this.showActionButtons && item.element.provider.actionButton) {
const button: ICompressedTreeElement<ISCMActionButton> = {
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;
return { element: item.element, children, incompressible: true, collapsed, collapsible: true };
@@ -1969,6 +2045,7 @@ export class SCMViewPane extends ViewPane {
const renderers: ICompressibleTreeRenderer<any, any, any>[] = [
this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)),
this.inputRenderer,
this.instantiationService.createInstance(ActionButtonRenderer),
this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)),
this.instantiationService.createInstance(ResourceRenderer, () => this._viewModel, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner)
];
@@ -2118,6 +2195,10 @@ export class SCMViewPane extends ViewPane {
}
}
return;
} else if (isSCMActionButton(e.element)) {
this.scmViewService.focus(e.element.repository);
return;
}
@@ -2170,7 +2251,7 @@ export class SCMViewPane extends ViewPane {
const menu = menus.repositoryMenu;
context = element.provider;
[actions, disposable] = collectContextMenuActions(menu);
} else if (isSCMInput(element)) {
} else if (isSCMInput(element) || isSCMActionButton(element)) {
// noop
} else if (isSCMResourceGroup(element)) {
const menus = this.scmViewService.menus.getRepositoryMenus(element.provider);
@@ -2310,3 +2391,49 @@ registerThemingParticipant((theme, collector) => {
collector.addRule(`.scm-view .scm-provider > .status > .monaco-action-bar > .actions-container { border-color: ${repositoryStatusActionsBorderColor}; }`);
}
});
export class ScmActionButton implements IDisposable {
private button: Button | undefined;
private readonly disposables = new MutableDisposable<DisposableStore>();
constructor(
private readonly container: HTMLElement,
private readonly commandService: ICommandService,
private readonly themeService: IThemeService,
private readonly notificationService: INotificationService
) {
}
dispose(): void {
this.disposables?.dispose();
}
setCommand(command: Command | undefined): void {
// Clear old button
this.clear();
if (!command) {
return;
}
this.button = new Button(this.container, { title: command.tooltip, supportIcons: true });
this.button.label = command.title;
this.button.onDidClick(async () => {
try {
await this.commandService.executeCommand(command!.id, ...(command!.arguments || []));
} catch (ex) {
this.notificationService.error(ex);
}
}, null, this.disposables.value);
this.disposables.value!.add(this.button);
this.disposables.value!.add(attachButtonStyler(this.button, this.themeService));
}
private clear(): void {
this.disposables.value = new DisposableStore();
this.button = undefined;
clearNode(this.container);
}
}
+5 -1
View File
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput } from 'vs/workbench/contrib/scm/common/scm';
import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton } 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, Disposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
@@ -25,6 +25,10 @@ export function isSCMInput(element: any): element is ISCMInput {
return !!(element as ISCMInput).validateInput && typeof (element as ISCMInput).value === 'string';
}
export function isSCMActionButton(element: any): element is ISCMActionButton {
return (element as ISCMActionButton).type === 'actionButton';
}
export function isSCMResourceGroup(element: any): element is ISCMResourceGroup {
return !!(element as ISCMResourceGroup).provider && !!(element as ISCMResourceGroup).elements;
}
@@ -65,6 +65,7 @@ export interface ISCMProvider extends IDisposable {
readonly onDidChangeCommitTemplate: Event<string>;
readonly onDidChangeStatusBarCommands?: Event<Command[]>;
readonly acceptInputCommand?: Command;
readonly actionButton?: Command;
readonly statusBarCommands?: Command[];
readonly onDidChange: Event<void>;
@@ -96,6 +97,12 @@ export interface ISCMInputChangeEvent {
readonly reason?: SCMInputChangeReason;
}
export interface ISCMActionButton {
readonly type: 'actionButton';
readonly repository: ISCMRepository;
readonly button?: Command;
}
export interface ISCMInput {
readonly repository: ISCMRepository;