mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-31 09:10:30 -04:00
Merge from vscode 817eb6b0c720a4ecbc13c020afbbebfed667aa09 (#7356)
This commit is contained in:
@@ -149,7 +149,7 @@ function getChangeTypeColor(theme: ITheme, changeType: ChangeType): Color | unde
|
||||
}
|
||||
}
|
||||
|
||||
function getOuterEditorFromDiffEditor(accessor: ServicesAccessor): ICodeEditor | null {
|
||||
function getOuterEditorFromDiffEditor(accessor: ServicesAccessor): ICodeEditor | undefined {
|
||||
const diffEditors = accessor.get(ICodeEditorService).listDiffEditors();
|
||||
|
||||
for (const diffEditor of diffEditors) {
|
||||
@@ -250,8 +250,8 @@ class DirtyDiffWidget extends PeekViewWidget {
|
||||
protected _fillHead(container: HTMLElement): void {
|
||||
super._fillHead(container);
|
||||
|
||||
const previous = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowPreviousChangeAction(), 'show-previous-change chevron-up');
|
||||
const next = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowNextChangeAction(), 'show-next-change chevron-down');
|
||||
const previous = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowPreviousChangeAction(), 'codicon-arrow-up');
|
||||
const next = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowNextChangeAction(), 'codicon-arrow-down');
|
||||
|
||||
this._disposables.add(previous);
|
||||
this._disposables.add(next);
|
||||
@@ -962,7 +962,7 @@ export class DirtyDiffModel extends Disposable {
|
||||
private repositoryDisposables = new Set<IDisposable>();
|
||||
private readonly originalModelDisposables = this._register(new DisposableStore());
|
||||
|
||||
private _onDidChange = new Emitter<ISplice<IChange>[]>();
|
||||
private readonly _onDidChange = new Emitter<ISplice<IChange>[]>();
|
||||
readonly onDidChange: Event<ISplice<IChange>[]> = this._onDidChange.event;
|
||||
|
||||
private _changes: IChange[] = [];
|
||||
|
||||
329
src/vs/workbench/contrib/scm/browser/mainPanel.ts
Normal file
329
src/vs/workbench/contrib/scm/browser/mainPanel.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/scmViewlet';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
import { IDisposable, dispose, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { append, $, toggleClass } from 'vs/base/browser/dom';
|
||||
import { List } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent, IListEvent } from 'vs/base/browser/ui/list/list';
|
||||
import { ISCMService, ISCMRepository } from 'vs/workbench/contrib/scm/common/scm';
|
||||
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { IAction, Action } from 'vs/base/common/actions';
|
||||
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
|
||||
import { Command } from 'vs/editor/common/modes';
|
||||
import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel';
|
||||
import { WorkbenchList } from 'vs/platform/list/browser/listService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IViewDescriptor } from 'vs/workbench/common/views';
|
||||
|
||||
export interface ISpliceEvent<T> {
|
||||
index: number;
|
||||
deleteCount: number;
|
||||
elements: T[];
|
||||
}
|
||||
|
||||
export interface IViewModel {
|
||||
readonly repositories: ISCMRepository[];
|
||||
readonly onDidSplice: Event<ISpliceEvent<ISCMRepository>>;
|
||||
|
||||
readonly visibleRepositories: ISCMRepository[];
|
||||
readonly onDidChangeVisibleRepositories: Event<ISCMRepository[]>;
|
||||
setVisibleRepositories(repositories: ISCMRepository[]): void;
|
||||
|
||||
isVisible(): boolean;
|
||||
readonly onDidChangeVisibility: Event<boolean>;
|
||||
}
|
||||
|
||||
class ProvidersListDelegate implements IListVirtualDelegate<ISCMRepository> {
|
||||
|
||||
getHeight(): number {
|
||||
return 22;
|
||||
}
|
||||
|
||||
getTemplateId(): string {
|
||||
return 'provider';
|
||||
}
|
||||
}
|
||||
|
||||
class StatusBarAction extends Action {
|
||||
|
||||
constructor(
|
||||
private command: Command,
|
||||
private commandService: ICommandService
|
||||
) {
|
||||
super(`statusbaraction{${command.id}}`, command.title, '', true);
|
||||
this.tooltip = command.tooltip || '';
|
||||
}
|
||||
|
||||
run(): Promise<void> {
|
||||
return this.commandService.executeCommand(this.command.id, ...(this.command.arguments || []));
|
||||
}
|
||||
}
|
||||
|
||||
class StatusBarActionViewItem extends ActionViewItem {
|
||||
|
||||
constructor(action: StatusBarAction) {
|
||||
super(null, action, {});
|
||||
}
|
||||
|
||||
updateLabel(): void {
|
||||
if (this.options.label) {
|
||||
this.label.innerHTML = renderOcticons(this.getAction().label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface RepositoryTemplateData {
|
||||
title: HTMLElement;
|
||||
type: HTMLElement;
|
||||
countContainer: HTMLElement;
|
||||
count: CountBadge;
|
||||
actionBar: ActionBar;
|
||||
disposable: IDisposable;
|
||||
templateDisposable: IDisposable;
|
||||
}
|
||||
|
||||
class ProviderRenderer implements IListRenderer<ISCMRepository, RepositoryTemplateData> {
|
||||
|
||||
readonly templateId = 'provider';
|
||||
|
||||
private readonly _onDidRenderElement = new Emitter<ISCMRepository>();
|
||||
readonly onDidRenderElement = this._onDidRenderElement.event;
|
||||
|
||||
constructor(
|
||||
@ICommandService protected commandService: ICommandService,
|
||||
@IThemeService protected themeService: IThemeService
|
||||
) { }
|
||||
|
||||
renderTemplate(container: HTMLElement): RepositoryTemplateData {
|
||||
const provider = append(container, $('.scm-provider'));
|
||||
const name = append(provider, $('.name'));
|
||||
const title = append(name, $('span.title'));
|
||||
const type = append(name, $('span.type'));
|
||||
const countContainer = append(provider, $('.count'));
|
||||
const count = new CountBadge(countContainer);
|
||||
const badgeStyler = attachBadgeStyler(count, this.themeService);
|
||||
const actionBar = new ActionBar(provider, { actionViewItemProvider: a => new StatusBarActionViewItem(a as StatusBarAction) });
|
||||
const disposable = Disposable.None;
|
||||
const templateDisposable = combinedDisposable(actionBar, badgeStyler);
|
||||
|
||||
return { title, type, countContainer, count, actionBar, disposable, templateDisposable };
|
||||
}
|
||||
|
||||
renderElement(repository: ISCMRepository, index: number, templateData: RepositoryTemplateData): void {
|
||||
templateData.disposable.dispose();
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
if (repository.provider.rootUri) {
|
||||
templateData.title.textContent = basename(repository.provider.rootUri);
|
||||
templateData.type.textContent = repository.provider.label;
|
||||
} else {
|
||||
templateData.title.textContent = repository.provider.label;
|
||||
templateData.type.textContent = '';
|
||||
}
|
||||
|
||||
const actions: IAction[] = [];
|
||||
const disposeActions = () => dispose(actions);
|
||||
disposables.add({ dispose: disposeActions });
|
||||
|
||||
const update = () => {
|
||||
disposeActions();
|
||||
|
||||
const commands = repository.provider.statusBarCommands || [];
|
||||
actions.splice(0, actions.length, ...commands.map(c => new StatusBarAction(c, this.commandService)));
|
||||
templateData.actionBar.clear();
|
||||
templateData.actionBar.push(actions);
|
||||
|
||||
const count = repository.provider.count || 0;
|
||||
toggleClass(templateData.countContainer, 'hidden', count === 0);
|
||||
templateData.count.setCount(count);
|
||||
|
||||
this._onDidRenderElement.fire(repository);
|
||||
};
|
||||
|
||||
disposables.add(repository.provider.onDidChange(update, null));
|
||||
update();
|
||||
|
||||
templateData.disposable = disposables;
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: RepositoryTemplateData): void {
|
||||
templateData.disposable.dispose();
|
||||
templateData.templateDisposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class MainPanel extends ViewletPanel {
|
||||
|
||||
static readonly ID = 'scm.mainPanel';
|
||||
static readonly TITLE = localize('scm providers', "Source Control Providers");
|
||||
|
||||
private list: List<ISCMRepository>;
|
||||
|
||||
constructor(
|
||||
protected viewModel: IViewModel,
|
||||
options: IViewletPanelOptions,
|
||||
@IKeybindingService protected keybindingService: IKeybindingService,
|
||||
@IContextMenuService protected contextMenuService: IContextMenuService,
|
||||
@ISCMService protected scmService: ISCMService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IMenuService private readonly menuService: IMenuService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService);
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
const delegate = new ProvidersListDelegate();
|
||||
const renderer = this.instantiationService.createInstance(ProviderRenderer);
|
||||
const identityProvider = { getId: (r: ISCMRepository) => r.provider.id };
|
||||
|
||||
this.list = this.instantiationService.createInstance(WorkbenchList, `SCM Main`, container, delegate, [renderer], {
|
||||
identityProvider,
|
||||
horizontalScrolling: false
|
||||
});
|
||||
|
||||
this._register(renderer.onDidRenderElement(e => this.list.updateWidth(this.viewModel.repositories.indexOf(e)), null));
|
||||
this._register(this.list.onSelectionChange(this.onListSelectionChange, this));
|
||||
this._register(this.list.onFocusChange(this.onListFocusChange, this));
|
||||
this._register(this.list.onContextMenu(this.onListContextMenu, this));
|
||||
|
||||
this._register(this.viewModel.onDidChangeVisibleRepositories(this.updateListSelection, this));
|
||||
|
||||
this._register(this.viewModel.onDidSplice(({ index, deleteCount, elements }) => this.splice(index, deleteCount, elements), null));
|
||||
this.splice(0, 0, this.viewModel.repositories);
|
||||
|
||||
this._register(this.list);
|
||||
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('scm.providers.visible')) {
|
||||
this.updateBodySize();
|
||||
}
|
||||
}));
|
||||
|
||||
this.updateListSelection();
|
||||
}
|
||||
|
||||
private splice(index: number, deleteCount: number, repositories: ISCMRepository[] = []): void {
|
||||
this.list.splice(index, deleteCount, repositories);
|
||||
|
||||
const empty = this.list.length === 0;
|
||||
toggleClass(this.element, 'empty', empty);
|
||||
|
||||
this.updateBodySize();
|
||||
}
|
||||
|
||||
protected layoutBody(height: number, width: number): void {
|
||||
this.list.layout(height, width);
|
||||
}
|
||||
|
||||
private updateBodySize(): void {
|
||||
const visibleCount = this.configurationService.getValue<number>('scm.providers.visible');
|
||||
const empty = this.list.length === 0;
|
||||
const size = Math.min(this.viewModel.repositories.length, visibleCount) * 22;
|
||||
|
||||
this.minimumBodySize = visibleCount === 0 ? 22 : size;
|
||||
this.maximumBodySize = visibleCount === 0 ? Number.POSITIVE_INFINITY : empty ? Number.POSITIVE_INFINITY : size;
|
||||
}
|
||||
|
||||
private onListContextMenu(e: IListContextMenuEvent<ISCMRepository>): void {
|
||||
if (!e.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const repository = e.element;
|
||||
const contextKeyService = this.contextKeyService.createScoped();
|
||||
const scmProviderKey = contextKeyService.createKey<string | undefined>('scmProvider', undefined);
|
||||
scmProviderKey.set(repository.provider.contextValue);
|
||||
|
||||
const menu = this.menuService.createMenu(MenuId.SCMSourceControl, contextKeyService);
|
||||
const primary: IAction[] = [];
|
||||
const secondary: IAction[] = [];
|
||||
const result = { primary, secondary };
|
||||
|
||||
const disposable = createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => g === 'inline');
|
||||
|
||||
menu.dispose();
|
||||
contextKeyService.dispose();
|
||||
|
||||
if (secondary.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => e.anchor,
|
||||
getActions: () => secondary,
|
||||
getActionsContext: () => repository.provider
|
||||
});
|
||||
|
||||
disposable.dispose();
|
||||
}
|
||||
|
||||
private onListSelectionChange(e: IListEvent<ISCMRepository>): void {
|
||||
if (e.browserEvent && e.elements.length > 0) {
|
||||
const scrollTop = this.list.scrollTop;
|
||||
this.viewModel.setVisibleRepositories(e.elements);
|
||||
this.list.scrollTop = scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
private onListFocusChange(e: IListEvent<ISCMRepository>): void {
|
||||
if (e.browserEvent && e.elements.length > 0) {
|
||||
e.elements[0].focus();
|
||||
}
|
||||
}
|
||||
|
||||
private updateListSelection(): void {
|
||||
const set = new Set();
|
||||
|
||||
for (const repository of this.viewModel.visibleRepositories) {
|
||||
set.add(repository);
|
||||
}
|
||||
|
||||
const selection: number[] = [];
|
||||
|
||||
for (let i = 0; i < this.list.length; i++) {
|
||||
if (set.has(this.list.element(i))) {
|
||||
selection.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
this.list.setSelection(selection);
|
||||
|
||||
if (selection.length > 0) {
|
||||
this.list.setFocus([selection[0]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MainPanelDescriptor implements IViewDescriptor {
|
||||
|
||||
readonly id = MainPanel.ID;
|
||||
readonly name = MainPanel.TITLE;
|
||||
readonly ctorDescriptor: { ctor: any, arguments?: any[] };
|
||||
readonly canToggleVisibility = true;
|
||||
readonly hideByDefault = false;
|
||||
readonly order = -1000;
|
||||
readonly workspace = true;
|
||||
readonly when = ContextKeyExpr.or(ContextKeyExpr.equals('config.scm.alwaysShowProviders', true), ContextKeyExpr.and(ContextKeyExpr.notEquals('scm.providerCount', 0), ContextKeyExpr.notEquals('scm.providerCount', 1)));
|
||||
|
||||
constructor(viewModel: IViewModel) {
|
||||
this.ctorDescriptor = { ctor: MainPanel, arguments: [viewModel] };
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
padding: 0 12px 0 20px;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row > .scm-provider > .monaco-action-bar {
|
||||
@@ -60,16 +61,16 @@
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row {
|
||||
padding: 0 12px 0 20px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row > .resource-group {
|
||||
.scm-viewlet .monaco-list-row .resource-group {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row > .resource-group > .name {
|
||||
.scm-viewlet .monaco-list-row .resource-group > .name {
|
||||
flex: 1;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
@@ -77,61 +78,66 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row > .resource {
|
||||
.scm-viewlet .monaco-list-row .resource {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row > .resource.faded {
|
||||
.scm-viewlet .monaco-list-row .resource.faded {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row > .resource > .name {
|
||||
.scm-viewlet .monaco-list-row .resource > .name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row > .resource > .name.strike-through > .monaco-icon-label > .monaco-icon-label-description-container > .label-name {
|
||||
.scm-viewlet .monaco-list-row .resource > .name.strike-through > .monaco-icon-label > .monaco-icon-label-description-container > .label-name {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row > .resource > .name > .monaco-icon-label::after {
|
||||
padding: 0 4px;
|
||||
.scm-viewlet .monaco-list-row .resource > .name > .monaco-icon-label::after {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row > .resource > .decoration-icon {
|
||||
.scm-viewlet .monaco-list-row .resource-group > .count {
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row .resource > .decoration-icon {
|
||||
width: 16px;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list .monaco-list-row > .resource > .name > .monaco-icon-label > .actions {
|
||||
.scm-viewlet .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions {
|
||||
flex-grow: 100;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list .monaco-list-row > .resource-group > .actions,
|
||||
.scm-viewlet .monaco-list .monaco-list-row > .resource > .name > .monaco-icon-label > .actions {
|
||||
.scm-viewlet .monaco-list .monaco-list-row .resource-group > .actions,
|
||||
.scm-viewlet .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list .monaco-list-row:hover > .resource-group > .actions,
|
||||
.scm-viewlet .monaco-list .monaco-list-row:hover > .resource > .name > .monaco-icon-label > .actions,
|
||||
.scm-viewlet .monaco-list .monaco-list-row.selected > .resource-group > .actions,
|
||||
.scm-viewlet .monaco-list .monaco-list-row.focused > .resource-group > .actions,
|
||||
.scm-viewlet .monaco-list .monaco-list-row.selected > .resource > .name > .monaco-icon-label > .actions,
|
||||
.scm-viewlet .monaco-list .monaco-list-row.focused > .resource > .name > .monaco-icon-label > .actions,
|
||||
.scm-viewlet .monaco-list:not(.selection-multiple) .monaco-list-row > .resource:hover > .actions {
|
||||
.scm-viewlet .monaco-list .monaco-list-row:hover .resource-group > .actions,
|
||||
.scm-viewlet .monaco-list .monaco-list-row:hover .resource > .name > .monaco-icon-label > .actions,
|
||||
.scm-viewlet .monaco-list .monaco-list-row.selected .resource-group > .actions,
|
||||
.scm-viewlet .monaco-list .monaco-list-row.focused .resource-group > .actions,
|
||||
.scm-viewlet .monaco-list .monaco-list-row.selected .resource > .name > .monaco-icon-label > .actions,
|
||||
.scm-viewlet .monaco-list .monaco-list-row.focused .resource > .name > .monaco-icon-label > .actions,
|
||||
.scm-viewlet .monaco-list:not(.selection-multiple) .monaco-list-row .resource:hover > .actions {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.scm-viewlet .scm-status.show-actions > .monaco-list .monaco-list-row > .resource-group > .actions,
|
||||
.scm-viewlet .scm-status.show-actions > .monaco-list .monaco-list-row > .resource > .name > .monaco-icon-label > .actions {
|
||||
.scm-viewlet .scm-status.show-actions > .monaco-list .monaco-list-row .resource-group > .actions,
|
||||
.scm-viewlet .scm-status.show-actions > .monaco-list .monaco-list-row .resource > .name > .monaco-icon-label > .actions {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.scm-viewlet .monaco-list-row > .resource > .name > .monaco-icon-label > .actions .action-label,
|
||||
.scm-viewlet .monaco-list-row > .resource-group > .actions .action-label {
|
||||
.scm-viewlet .monaco-list-row .resource > .name > .monaco-icon-label > .actions .action-label,
|
||||
.scm-viewlet .monaco-list-row .resource-group > .actions .action-label {
|
||||
width: 16px;
|
||||
height: 100%;
|
||||
background-position: 50% 50%;
|
||||
@@ -162,3 +168,9 @@
|
||||
.scm-viewlet .scm-editor.scroll > .monaco-inputbox > .wrapper > textarea.input {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.scm-viewlet .list-view-mode .monaco-tl-twistie:not(.force-twistie):not(.collapsible) {
|
||||
background-image: none !important;
|
||||
width: 8px !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
|
||||
import 'vs/css!./media/scmViewlet';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { createAndFillInContextMenuActions, createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { ISCMProvider, ISCMResource, ISCMResourceGroup } from 'vs/workbench/contrib/scm/common/scm';
|
||||
import { isSCMResource } from './scmUtil';
|
||||
import { isSCMResource } from './util';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { ISplice } from 'vs/base/common/sequence';
|
||||
@@ -20,13 +20,15 @@ function actionEquals(a: IAction, b: IAction): boolean {
|
||||
return a.id === b.id;
|
||||
}
|
||||
|
||||
interface ISCMResourceGroupMenuEntry extends IDisposable {
|
||||
interface ISCMResourceGroupMenuEntry {
|
||||
readonly group: ISCMResourceGroup;
|
||||
readonly disposable: IDisposable;
|
||||
}
|
||||
|
||||
interface ISCMMenus {
|
||||
readonly resourceGroupMenu: IMenu;
|
||||
readonly resourceMenu: IMenu;
|
||||
readonly resourceFolderMenu: IMenu;
|
||||
}
|
||||
|
||||
export function getSCMResourceContextKey(resource: ISCMResourceGroup | ISCMResource): string {
|
||||
@@ -48,7 +50,7 @@ export class SCMMenus implements IDisposable {
|
||||
private readonly resourceGroupMenuEntries: ISCMResourceGroupMenuEntry[] = [];
|
||||
private readonly resourceGroupMenus = new Map<ISCMResourceGroup, ISCMMenus>();
|
||||
|
||||
private readonly disposables: IDisposable[] = [];
|
||||
private readonly disposables = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
provider: ISCMProvider | undefined,
|
||||
@@ -68,7 +70,7 @@ export class SCMMenus implements IDisposable {
|
||||
}
|
||||
|
||||
this.titleMenu = this.menuService.createMenu(MenuId.SCMTitle, this.contextKeyService);
|
||||
this.disposables.push(this.titleMenu);
|
||||
this.disposables.add(this.titleMenu);
|
||||
|
||||
this.titleMenu.onDidChange(this.updateTitleActions, this, this.disposables);
|
||||
this.updateTitleActions();
|
||||
@@ -109,6 +111,10 @@ export class SCMMenus implements IDisposable {
|
||||
return this.getActions(MenuId.SCMResourceContext, resource).secondary;
|
||||
}
|
||||
|
||||
getResourceFolderContextActions(group: ISCMResourceGroup): IAction[] {
|
||||
return this.getActions(MenuId.SCMResourceFolderContext, group).secondary;
|
||||
}
|
||||
|
||||
private getActions(menuId: MenuId, resource: ISCMResourceGroup | ISCMResource): { primary: IAction[]; secondary: IAction[]; } {
|
||||
const contextKeyService = this.contextKeyService.createScoped();
|
||||
contextKeyService.createKey('scmResourceGroup', getSCMResourceContextKey(resource));
|
||||
@@ -141,6 +147,14 @@ export class SCMMenus implements IDisposable {
|
||||
return this.resourceGroupMenus.get(group)!.resourceMenu;
|
||||
}
|
||||
|
||||
getResourceFolderMenu(group: ISCMResourceGroup): IMenu {
|
||||
if (!this.resourceGroupMenus.has(group)) {
|
||||
throw new Error('SCM Resource Group menu not found');
|
||||
}
|
||||
|
||||
return this.resourceGroupMenus.get(group)!.resourceFolderMenu;
|
||||
}
|
||||
|
||||
private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice<ISCMResourceGroup>): void {
|
||||
const menuEntriesToInsert = toInsert.map<ISCMResourceGroupMenuEntry>(group => {
|
||||
const contextKeyService = this.contextKeyService.createScoped();
|
||||
@@ -149,30 +163,23 @@ export class SCMMenus implements IDisposable {
|
||||
|
||||
const resourceGroupMenu = this.menuService.createMenu(MenuId.SCMResourceGroupContext, contextKeyService);
|
||||
const resourceMenu = this.menuService.createMenu(MenuId.SCMResourceContext, contextKeyService);
|
||||
const resourceFolderMenu = this.menuService.createMenu(MenuId.SCMResourceFolderContext, contextKeyService);
|
||||
const disposable = combinedDisposable(contextKeyService, resourceGroupMenu, resourceMenu, resourceFolderMenu);
|
||||
|
||||
this.resourceGroupMenus.set(group, { resourceGroupMenu, resourceMenu });
|
||||
|
||||
return {
|
||||
group,
|
||||
dispose() {
|
||||
contextKeyService.dispose();
|
||||
resourceGroupMenu.dispose();
|
||||
resourceMenu.dispose();
|
||||
}
|
||||
};
|
||||
this.resourceGroupMenus.set(group, { resourceGroupMenu, resourceMenu, resourceFolderMenu });
|
||||
return { group, disposable };
|
||||
});
|
||||
|
||||
const deleted = this.resourceGroupMenuEntries.splice(start, deleteCount, ...menuEntriesToInsert);
|
||||
|
||||
for (const entry of deleted) {
|
||||
this.resourceGroupMenus.delete(entry.group);
|
||||
entry.dispose();
|
||||
entry.disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this.disposables);
|
||||
dispose(this.resourceGroupMenuEntries);
|
||||
this.resourceGroupMenus.clear();
|
||||
this.disposables.dispose();
|
||||
this.resourceGroupMenuEntries.forEach(e => e.disposable.dispose());
|
||||
}
|
||||
}
|
||||
864
src/vs/workbench/contrib/scm/browser/repositoryPanel.ts
Normal file
864
src/vs/workbench/contrib/scm/browser/repositoryPanel.ts
Normal file
@@ -0,0 +1,864 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/scmViewlet';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
import { IDisposable, Disposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ViewletPanel, IViewletPanelOptions } from 'vs/workbench/browser/parts/views/panelViewlet';
|
||||
import { append, $, addClass, toggleClass, trackFocus, removeClass } from 'vs/base/browser/dom';
|
||||
import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list';
|
||||
import { ISCMRepository, ISCMResourceGroup, ISCMResource, InputValidationType } 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';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { MenuItemAction, IMenuService } from 'vs/platform/actions/common/actions';
|
||||
import { IAction, IActionViewItem, ActionRunner, Action } from 'vs/base/common/actions';
|
||||
import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { SCMMenus } from './menus';
|
||||
import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IThemeService, LIGHT } from 'vs/platform/theme/common/themeService';
|
||||
import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar } from './util';
|
||||
import { attachBadgeStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { format } from 'vs/base/common/strings';
|
||||
import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ThrottledDelayer } from 'vs/base/common/async';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ISequence, ISplice } from 'vs/base/common/sequence';
|
||||
import { ResourceTree, IBranchNode, INode } from 'vs/base/common/resourceTree';
|
||||
import { ObjectTree, ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/tree/objectTree';
|
||||
import { Iterator } from 'vs/base/common/iterator';
|
||||
import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { FileKind } from 'vs/platform/files/common/files';
|
||||
import { compareFileNames } from 'vs/base/common/comparers';
|
||||
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
|
||||
import { IViewDescriptor } from 'vs/workbench/common/views';
|
||||
import { localize } from 'vs/nls';
|
||||
import { flatten } from 'vs/base/common/arrays';
|
||||
import { memoize } from 'vs/base/common/decorators';
|
||||
import { IWorkbenchThemeService, IFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
|
||||
|
||||
type TreeElement = ISCMResourceGroup | IBranchNode<ISCMResource, ISCMResourceGroup> | ISCMResource;
|
||||
|
||||
interface ResourceGroupTemplate {
|
||||
readonly name: HTMLElement;
|
||||
readonly count: CountBadge;
|
||||
readonly actionBar: ActionBar;
|
||||
elementDisposables: IDisposable;
|
||||
readonly disposables: IDisposable;
|
||||
}
|
||||
|
||||
class ResourceGroupRenderer implements ICompressibleTreeRenderer<ISCMResourceGroup, FuzzyScore, ResourceGroupTemplate> {
|
||||
|
||||
static TEMPLATE_ID = 'resource group';
|
||||
get templateId(): string { return ResourceGroupRenderer.TEMPLATE_ID; }
|
||||
|
||||
constructor(
|
||||
private actionViewItemProvider: IActionViewItemProvider,
|
||||
private themeService: IThemeService,
|
||||
private menus: SCMMenus
|
||||
) { }
|
||||
|
||||
renderTemplate(container: HTMLElement): ResourceGroupTemplate {
|
||||
// hack
|
||||
addClass(container.parentElement!.parentElement!.querySelector('.monaco-tl-twistie')! as HTMLElement, 'force-twistie');
|
||||
|
||||
const element = append(container, $('.resource-group'));
|
||||
const name = append(element, $('.name'));
|
||||
const actionsContainer = append(element, $('.actions'));
|
||||
const actionBar = new ActionBar(actionsContainer, { actionViewItemProvider: this.actionViewItemProvider });
|
||||
const countContainer = append(element, $('.count'));
|
||||
const count = new CountBadge(countContainer);
|
||||
const styler = attachBadgeStyler(count, this.themeService);
|
||||
const elementDisposables = Disposable.None;
|
||||
const disposables = combinedDisposable(actionBar, styler);
|
||||
|
||||
return { name, count, actionBar, elementDisposables, disposables };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<ISCMResourceGroup, FuzzyScore>, index: number, template: ResourceGroupTemplate): void {
|
||||
template.elementDisposables.dispose();
|
||||
|
||||
const group = node.element;
|
||||
template.name.textContent = group.label;
|
||||
template.actionBar.clear();
|
||||
template.actionBar.context = group;
|
||||
template.count.setCount(group.elements.length);
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
disposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceGroupMenu(group), template.actionBar));
|
||||
|
||||
template.elementDisposables = disposables;
|
||||
}
|
||||
|
||||
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ISCMResourceGroup>, FuzzyScore>, index: number, templateData: ResourceGroupTemplate, height: number | undefined): void {
|
||||
throw new Error('Should never happen since node is incompressible');
|
||||
}
|
||||
|
||||
disposeElement(group: ITreeNode<ISCMResourceGroup, FuzzyScore>, index: number, template: ResourceGroupTemplate): void {
|
||||
template.elementDisposables.dispose();
|
||||
}
|
||||
|
||||
disposeTemplate(template: ResourceGroupTemplate): void {
|
||||
template.elementDisposables.dispose();
|
||||
template.disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
interface ResourceTemplate {
|
||||
element: HTMLElement;
|
||||
name: HTMLElement;
|
||||
fileLabel: IResourceLabel;
|
||||
decorationIcon: HTMLElement;
|
||||
actionBar: ActionBar;
|
||||
elementDisposables: IDisposable;
|
||||
disposables: IDisposable;
|
||||
}
|
||||
|
||||
class MultipleSelectionActionRunner extends ActionRunner {
|
||||
|
||||
constructor(private getSelectedResources: () => (ISCMResource | IBranchNode<ISCMResource, ISCMResourceGroup>)[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
runAction(action: IAction, context: ISCMResource | IBranchNode<ISCMResource, ISCMResourceGroup>): Promise<any> {
|
||||
if (!(action instanceof MenuItemAction)) {
|
||||
return super.runAction(action, context);
|
||||
}
|
||||
|
||||
const selection = this.getSelectedResources();
|
||||
const contextIsSelected = selection.some(s => s === context);
|
||||
const actualContext = contextIsSelected ? selection : [context];
|
||||
const args = flatten(actualContext.map(e => ResourceTree.isBranchNode(e) ? ResourceTree.collect(e) : [e]));
|
||||
return action.run(...args);
|
||||
}
|
||||
}
|
||||
|
||||
class ResourceRenderer implements ICompressibleTreeRenderer<ISCMResource | IBranchNode<ISCMResource, ISCMResourceGroup>, FuzzyScore, ResourceTemplate> {
|
||||
|
||||
static TEMPLATE_ID = 'resource';
|
||||
get templateId(): string { return ResourceRenderer.TEMPLATE_ID; }
|
||||
|
||||
constructor(
|
||||
private viewModelProvider: () => ViewModel,
|
||||
private labels: ResourceLabels,
|
||||
private actionViewItemProvider: IActionViewItemProvider,
|
||||
private getSelectedResources: () => (ISCMResource | IBranchNode<ISCMResource, ISCMResourceGroup>)[],
|
||||
private themeService: IThemeService,
|
||||
private menus: SCMMenus
|
||||
) { }
|
||||
|
||||
renderTemplate(container: HTMLElement): ResourceTemplate {
|
||||
const element = append(container, $('.resource'));
|
||||
const name = append(element, $('.name'));
|
||||
const fileLabel = this.labels.create(name, { supportHighlights: true });
|
||||
const actionsContainer = append(fileLabel.element, $('.actions'));
|
||||
const actionBar = new ActionBar(actionsContainer, {
|
||||
actionViewItemProvider: this.actionViewItemProvider,
|
||||
actionRunner: new MultipleSelectionActionRunner(this.getSelectedResources)
|
||||
});
|
||||
|
||||
const decorationIcon = append(element, $('.decoration-icon'));
|
||||
const disposables = combinedDisposable(actionBar, fileLabel);
|
||||
|
||||
return { element, name, fileLabel, decorationIcon, actionBar, elementDisposables: Disposable.None, disposables };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<ISCMResource, FuzzyScore> | ITreeNode<IBranchNode<ISCMResource, ISCMResourceGroup>, FuzzyScore>, index: number, template: ResourceTemplate): void {
|
||||
template.elementDisposables.dispose();
|
||||
|
||||
const elementDisposables = new DisposableStore();
|
||||
const resourceOrFolder = node.element;
|
||||
const theme = this.themeService.getTheme();
|
||||
const icon = !ResourceTree.isBranchNode(resourceOrFolder) && (theme.type === LIGHT ? resourceOrFolder.decorations.icon : resourceOrFolder.decorations.iconDark);
|
||||
|
||||
const uri = ResourceTree.isBranchNode(resourceOrFolder) ? resourceOrFolder.uri : resourceOrFolder.sourceUri;
|
||||
const fileKind = ResourceTree.isBranchNode(resourceOrFolder) ? FileKind.FOLDER : FileKind.FILE;
|
||||
const viewModel = this.viewModelProvider();
|
||||
|
||||
template.fileLabel.setFile(uri, {
|
||||
fileDecorations: { colors: false, badges: !icon },
|
||||
hidePath: viewModel.mode === ViewModelMode.Tree,
|
||||
fileKind,
|
||||
matches: createMatches(node.filterData)
|
||||
});
|
||||
|
||||
template.actionBar.clear();
|
||||
template.actionBar.context = resourceOrFolder;
|
||||
|
||||
if (ResourceTree.isBranchNode(resourceOrFolder)) {
|
||||
elementDisposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceFolderMenu(resourceOrFolder.context), template.actionBar));
|
||||
removeClass(template.name, 'strike-through');
|
||||
removeClass(template.element, 'faded');
|
||||
} else {
|
||||
elementDisposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceMenu(resourceOrFolder.resourceGroup), template.actionBar));
|
||||
toggleClass(template.name, 'strike-through', resourceOrFolder.decorations.strikeThrough);
|
||||
toggleClass(template.element, 'faded', resourceOrFolder.decorations.faded);
|
||||
}
|
||||
|
||||
const tooltip = !ResourceTree.isBranchNode(resourceOrFolder) && resourceOrFolder.decorations.tooltip || '';
|
||||
|
||||
if (icon) {
|
||||
template.decorationIcon.style.display = '';
|
||||
template.decorationIcon.style.backgroundImage = `url('${icon}')`;
|
||||
template.decorationIcon.title = tooltip;
|
||||
} else {
|
||||
template.decorationIcon.style.display = 'none';
|
||||
template.decorationIcon.style.backgroundImage = '';
|
||||
template.decorationIcon.title = '';
|
||||
}
|
||||
|
||||
template.element.setAttribute('data-tooltip', tooltip);
|
||||
template.elementDisposables = elementDisposables;
|
||||
}
|
||||
|
||||
disposeElement(resource: ITreeNode<ISCMResource, FuzzyScore> | ITreeNode<IBranchNode<ISCMResource, ISCMResourceGroup>, FuzzyScore>, index: number, template: ResourceTemplate): void {
|
||||
template.elementDisposables.dispose();
|
||||
}
|
||||
|
||||
renderCompressedElements(node: ITreeNode<ICompressedTreeNode<ISCMResource> | ICompressedTreeNode<IBranchNode<ISCMResource, ISCMResourceGroup>>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void {
|
||||
template.elementDisposables.dispose();
|
||||
|
||||
const elementDisposables = new DisposableStore();
|
||||
const compressed = node.element as ICompressedTreeNode<IBranchNode<ISCMResource, ISCMResourceGroup>>;
|
||||
const folder = compressed.elements[compressed.elements.length - 1];
|
||||
|
||||
const label = compressed.elements.map(e => e.name).join('/');
|
||||
const fileKind = FileKind.FOLDER;
|
||||
|
||||
template.fileLabel.setResource({ resource: folder.uri, name: label }, {
|
||||
fileDecorations: { colors: false, badges: true },
|
||||
fileKind,
|
||||
matches: createMatches(node.filterData)
|
||||
});
|
||||
|
||||
template.actionBar.clear();
|
||||
template.actionBar.context = folder;
|
||||
|
||||
elementDisposables.add(connectPrimaryMenuToInlineActionBar(this.menus.getResourceFolderMenu(folder.context), template.actionBar));
|
||||
|
||||
removeClass(template.name, 'strike-through');
|
||||
removeClass(template.element, 'faded');
|
||||
template.decorationIcon.style.display = 'none';
|
||||
template.decorationIcon.style.backgroundImage = '';
|
||||
|
||||
template.element.setAttribute('data-tooltip', '');
|
||||
template.elementDisposables = elementDisposables;
|
||||
}
|
||||
|
||||
disposeCompressedElements(node: ITreeNode<ICompressedTreeNode<ISCMResource> | ICompressedTreeNode<IBranchNode<ISCMResource, ISCMResourceGroup>>, FuzzyScore>, index: number, template: ResourceTemplate, height: number | undefined): void {
|
||||
template.elementDisposables.dispose();
|
||||
}
|
||||
|
||||
disposeTemplate(template: ResourceTemplate): void {
|
||||
template.elementDisposables.dispose();
|
||||
template.disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class ProviderListDelegate implements IListVirtualDelegate<TreeElement> {
|
||||
|
||||
getHeight() { return 22; }
|
||||
|
||||
getTemplateId(element: TreeElement) {
|
||||
if (ResourceTree.isBranchNode(element) || isSCMResource(element)) {
|
||||
return ResourceRenderer.TEMPLATE_ID;
|
||||
} else {
|
||||
return ResourceGroupRenderer.TEMPLATE_ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SCMTreeFilter implements ITreeFilter<TreeElement> {
|
||||
|
||||
filter(element: TreeElement): boolean {
|
||||
if (ResourceTree.isBranchNode(element)) {
|
||||
return true;
|
||||
} else if (isSCMResourceGroup(element)) {
|
||||
return element.elements.length > 0 || !element.hideWhenEmpty;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SCMTreeSorter implements ITreeSorter<TreeElement> {
|
||||
|
||||
@memoize
|
||||
private get viewModel(): ViewModel { return this.viewModelProvider(); }
|
||||
|
||||
constructor(private viewModelProvider: () => ViewModel) { }
|
||||
|
||||
compare(one: TreeElement, other: TreeElement): number {
|
||||
if (this.viewModel.mode === ViewModelMode.List) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isSCMResourceGroup(one) && isSCMResourceGroup(other)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const oneIsDirectory = ResourceTree.isBranchNode(one);
|
||||
const otherIsDirectory = ResourceTree.isBranchNode(other);
|
||||
|
||||
if (oneIsDirectory !== otherIsDirectory) {
|
||||
return oneIsDirectory ? -1 : 1;
|
||||
}
|
||||
|
||||
const oneName = ResourceTree.isBranchNode(one) ? one.name : basename((one as ISCMResource).sourceUri);
|
||||
const otherName = ResourceTree.isBranchNode(other) ? other.name : basename((other as ISCMResource).sourceUri);
|
||||
|
||||
return compareFileNames(oneName, otherName);
|
||||
}
|
||||
}
|
||||
|
||||
export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyboardNavigationLabelProvider<TreeElement> {
|
||||
|
||||
getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } | undefined {
|
||||
if (ResourceTree.isBranchNode(element)) {
|
||||
return element.name;
|
||||
} else if (isSCMResourceGroup(element)) {
|
||||
return element.label;
|
||||
} else {
|
||||
return basename(element.sourceUri);
|
||||
}
|
||||
}
|
||||
|
||||
getCompressedNodeKeyboardNavigationLabel(elements: TreeElement[]): { toString(): string | undefined; } | undefined {
|
||||
const folders = elements as IBranchNode<ISCMResource, ISCMResourceGroup>[];
|
||||
return folders.map(e => e.name).join('/');
|
||||
}
|
||||
}
|
||||
|
||||
class SCMResourceIdentityProvider implements IIdentityProvider<TreeElement> {
|
||||
|
||||
getId(element: TreeElement): string {
|
||||
if (ResourceTree.isBranchNode(element)) {
|
||||
const group = element.context;
|
||||
return `${group.provider.contextValue}/${group.id}/$FOLDER/${element.uri.toString()}`;
|
||||
} else if (isSCMResource(element)) {
|
||||
const group = element.resourceGroup;
|
||||
const provider = group.provider;
|
||||
return `${provider.contextValue}/${group.id}/${element.sourceUri.toString()}`;
|
||||
} else {
|
||||
const provider = element.provider;
|
||||
return `${provider.contextValue}/${element.id}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface IGroupItem {
|
||||
readonly group: ISCMResourceGroup;
|
||||
readonly resources: ISCMResource[];
|
||||
readonly tree: ResourceTree<ISCMResource, ISCMResourceGroup>;
|
||||
readonly disposable: IDisposable;
|
||||
}
|
||||
|
||||
function groupItemAsTreeElement(item: IGroupItem, mode: ViewModelMode): ICompressedTreeElement<TreeElement> {
|
||||
const children = mode === ViewModelMode.List
|
||||
? Iterator.map(Iterator.fromArray(item.resources), element => ({ element, incompressible: true }))
|
||||
: Iterator.map(item.tree.root.children, node => asTreeElement(node, true));
|
||||
|
||||
return { element: item.group, children, incompressible: true };
|
||||
}
|
||||
|
||||
function asTreeElement(node: INode<ISCMResource, ISCMResourceGroup>, incompressible: boolean): ICompressedTreeElement<TreeElement> {
|
||||
if (ResourceTree.isBranchNode(node)) {
|
||||
return {
|
||||
element: node,
|
||||
children: Iterator.map(node.children, node => asTreeElement(node, false)),
|
||||
incompressible,
|
||||
collapsed: false
|
||||
};
|
||||
}
|
||||
|
||||
return { element: node.element, incompressible: true };
|
||||
}
|
||||
|
||||
const enum ViewModelMode {
|
||||
List = 'codicon-filter',
|
||||
Tree = 'codicon-selection'
|
||||
}
|
||||
|
||||
class ViewModel {
|
||||
|
||||
private _mode = ViewModelMode.Tree;
|
||||
private readonly _onDidChangeMode = new Emitter<ViewModelMode>();
|
||||
readonly onDidChangeMode = this._onDidChangeMode.event;
|
||||
|
||||
get mode(): ViewModelMode { return this._mode; }
|
||||
set mode(mode: ViewModelMode) {
|
||||
this._mode = mode;
|
||||
this.refresh();
|
||||
this._onDidChangeMode.fire(mode);
|
||||
}
|
||||
|
||||
private items: IGroupItem[] = [];
|
||||
private visibilityDisposables = new DisposableStore();
|
||||
private scrollTop: number | undefined;
|
||||
private disposables = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
private groups: ISequence<ISCMResourceGroup>,
|
||||
private tree: ObjectTree<TreeElement, FuzzyScore>
|
||||
) { }
|
||||
|
||||
private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice<ISCMResourceGroup>): void {
|
||||
const itemsToInsert: IGroupItem[] = [];
|
||||
|
||||
for (const group of toInsert) {
|
||||
const tree = new ResourceTree<ISCMResource, ISCMResourceGroup>(group, group.provider.rootUri || URI.file('/'));
|
||||
const resources: ISCMResource[] = [...group.elements];
|
||||
const disposable = combinedDisposable(
|
||||
group.onDidChange(() => this.tree.refilter()),
|
||||
group.onDidSplice(splice => this.onDidSpliceGroup(item, splice))
|
||||
);
|
||||
|
||||
const item = { group, resources, tree, disposable };
|
||||
|
||||
for (const resource of resources) {
|
||||
item.tree.add(resource.sourceUri, resource);
|
||||
}
|
||||
|
||||
itemsToInsert.push(item);
|
||||
}
|
||||
|
||||
const itemsToDispose = this.items.splice(start, deleteCount, ...itemsToInsert);
|
||||
|
||||
for (const item of itemsToDispose) {
|
||||
item.disposable.dispose();
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private onDidSpliceGroup(item: IGroupItem, { start, deleteCount, toInsert }: ISplice<ISCMResource>): void {
|
||||
for (const resource of toInsert) {
|
||||
item.tree.add(resource.sourceUri, resource);
|
||||
}
|
||||
|
||||
const deleted = item.resources.splice(start, deleteCount, ...toInsert);
|
||||
|
||||
for (const resource of deleted) {
|
||||
item.tree.delete(resource.sourceUri);
|
||||
}
|
||||
|
||||
this.refresh(item);
|
||||
}
|
||||
|
||||
setVisible(visible: boolean): void {
|
||||
if (visible) {
|
||||
this.visibilityDisposables = new DisposableStore();
|
||||
this.groups.onDidSplice(this.onDidSpliceGroups, this, this.visibilityDisposables);
|
||||
this.onDidSpliceGroups({ start: 0, deleteCount: this.items.length, toInsert: this.groups.elements });
|
||||
|
||||
if (typeof this.scrollTop === 'number') {
|
||||
this.tree.scrollTop = this.scrollTop;
|
||||
this.scrollTop = undefined;
|
||||
}
|
||||
} else {
|
||||
this.visibilityDisposables.dispose();
|
||||
this.onDidSpliceGroups({ start: 0, deleteCount: this.items.length, toInsert: [] });
|
||||
this.scrollTop = this.tree.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
private refresh(item?: IGroupItem): void {
|
||||
if (item) {
|
||||
this.tree.setChildren(item.group, groupItemAsTreeElement(item, this.mode).children);
|
||||
} else {
|
||||
this.tree.setChildren(null, this.items.map(item => groupItemAsTreeElement(item, this.mode)));
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.visibilityDisposables.dispose();
|
||||
this.disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class ToggleViewModeAction extends Action {
|
||||
|
||||
static readonly ID = 'workbench.scm.action.toggleViewMode';
|
||||
static readonly LABEL = localize('toggleViewMode', "Toggle View Mode");
|
||||
|
||||
constructor(private viewModel: ViewModel) {
|
||||
super(ToggleViewModeAction.ID, ToggleViewModeAction.LABEL);
|
||||
|
||||
this._register(this.viewModel.onDidChangeMode(this.onDidChangeMode, this));
|
||||
this.onDidChangeMode(this.viewModel.mode);
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
this.viewModel.mode = this.viewModel.mode === ViewModelMode.List ? ViewModelMode.Tree : ViewModelMode.List;
|
||||
}
|
||||
|
||||
private onDidChangeMode(mode: ViewModelMode): void {
|
||||
this.class = `scm-action toggle-view-mode ${mode}`;
|
||||
}
|
||||
}
|
||||
|
||||
function convertValidationType(type: InputValidationType): MessageType {
|
||||
switch (type) {
|
||||
case InputValidationType.Information: return MessageType.INFO;
|
||||
case InputValidationType.Warning: return MessageType.WARNING;
|
||||
case InputValidationType.Error: return MessageType.ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
export class RepositoryPanel extends ViewletPanel {
|
||||
|
||||
private cachedHeight: number | undefined = undefined;
|
||||
private cachedWidth: number | undefined = undefined;
|
||||
private inputBoxContainer: HTMLElement;
|
||||
private inputBox: InputBox;
|
||||
private listContainer: HTMLElement;
|
||||
private tree: ObjectTree<TreeElement, FuzzyScore>;
|
||||
private viewModel: ViewModel;
|
||||
private listLabels: ResourceLabels;
|
||||
private menus: SCMMenus;
|
||||
private toggleViewModelModeAction: ToggleViewModeAction | undefined;
|
||||
protected contextKeyService: IContextKeyService;
|
||||
|
||||
constructor(
|
||||
readonly repository: ISCMRepository,
|
||||
options: IViewletPanelOptions,
|
||||
@IKeybindingService protected keybindingService: IKeybindingService,
|
||||
@IWorkbenchThemeService protected themeService: IWorkbenchThemeService,
|
||||
@IContextMenuService protected contextMenuService: IContextMenuService,
|
||||
@IContextViewService protected contextViewService: IContextViewService,
|
||||
@ICommandService protected commandService: ICommandService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IEditorService protected editorService: IEditorService,
|
||||
@IInstantiationService protected instantiationService: IInstantiationService,
|
||||
@IConfigurationService protected configurationService: IConfigurationService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IMenuService protected menuService: IMenuService
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService);
|
||||
|
||||
this.menus = instantiationService.createInstance(SCMMenus, this.repository.provider);
|
||||
this._register(this.menus);
|
||||
this._register(this.menus.onDidChangeTitle(this._onDidChangeTitleArea.fire, this._onDidChangeTitleArea));
|
||||
|
||||
this.contextKeyService = contextKeyService.createScoped(this.element);
|
||||
this.contextKeyService.createKey('scmRepository', this.repository);
|
||||
}
|
||||
|
||||
render(): void {
|
||||
super.render();
|
||||
this._register(this.menus.onDidChangeTitle(this.updateActions, this));
|
||||
}
|
||||
|
||||
protected renderHeaderTitle(container: HTMLElement): void {
|
||||
let title: string;
|
||||
let type: string;
|
||||
|
||||
if (this.repository.provider.rootUri) {
|
||||
title = basename(this.repository.provider.rootUri);
|
||||
type = this.repository.provider.label;
|
||||
} else {
|
||||
title = this.repository.provider.label;
|
||||
type = '';
|
||||
}
|
||||
|
||||
super.renderHeaderTitle(container, title);
|
||||
addClass(container, 'scm-provider');
|
||||
append(container, $('span.type', undefined, type));
|
||||
}
|
||||
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
const focusTracker = trackFocus(container);
|
||||
this._register(focusTracker.onDidFocus(() => this.repository.focus()));
|
||||
this._register(focusTracker);
|
||||
|
||||
// Input
|
||||
this.inputBoxContainer = append(container, $('.scm-editor'));
|
||||
|
||||
const updatePlaceholder = () => {
|
||||
const binding = this.keybindingService.lookupKeybinding('scm.acceptInput');
|
||||
const label = binding ? binding.getLabel() : (platform.isMacintosh ? 'Cmd+Enter' : 'Ctrl+Enter');
|
||||
const placeholder = format(this.repository.input.placeholder, label);
|
||||
|
||||
this.inputBox.setPlaceHolder(placeholder);
|
||||
};
|
||||
|
||||
const validationDelayer = new ThrottledDelayer<any>(200);
|
||||
const validate = () => {
|
||||
return this.repository.input.validateInput(this.inputBox.value, this.inputBox.inputElement.selectionStart || 0).then(result => {
|
||||
if (!result) {
|
||||
this.inputBox.inputElement.removeAttribute('aria-invalid');
|
||||
this.inputBox.hideMessage();
|
||||
} else {
|
||||
this.inputBox.inputElement.setAttribute('aria-invalid', 'true');
|
||||
this.inputBox.showMessage({ content: result.message, type: convertValidationType(result.type) });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const triggerValidation = () => validationDelayer.trigger(validate);
|
||||
|
||||
this.inputBox = new InputBox(this.inputBoxContainer, this.contextViewService, { flexibleHeight: true, flexibleMaxHeight: 134 });
|
||||
this.inputBox.setEnabled(this.isBodyVisible());
|
||||
this._register(attachInputBoxStyler(this.inputBox, this.themeService));
|
||||
this._register(this.inputBox);
|
||||
|
||||
this._register(this.inputBox.onDidChange(triggerValidation, null));
|
||||
|
||||
const onKeyUp = domEvent(this.inputBox.inputElement, 'keyup');
|
||||
const onMouseUp = domEvent(this.inputBox.inputElement, 'mouseup');
|
||||
this._register(Event.any<any>(onKeyUp, onMouseUp)(triggerValidation, null));
|
||||
|
||||
this.inputBox.value = this.repository.input.value;
|
||||
this._register(this.inputBox.onDidChange(value => this.repository.input.value = value, null));
|
||||
this._register(this.repository.input.onDidChange(value => this.inputBox.value = value, null));
|
||||
|
||||
updatePlaceholder();
|
||||
this._register(this.repository.input.onDidChangePlaceholder(updatePlaceholder, null));
|
||||
this._register(this.keybindingService.onDidUpdateKeybindings(updatePlaceholder, null));
|
||||
|
||||
this._register(this.inputBox.onDidHeightChange(() => this.layoutBody()));
|
||||
|
||||
if (this.repository.provider.onDidChangeCommitTemplate) {
|
||||
this._register(this.repository.provider.onDidChangeCommitTemplate(this.updateInputBox, this));
|
||||
}
|
||||
|
||||
this.updateInputBox();
|
||||
|
||||
// Input box visibility
|
||||
this._register(this.repository.input.onDidChangeVisibility(this.updateInputBoxVisibility, this));
|
||||
this.updateInputBoxVisibility();
|
||||
|
||||
// List
|
||||
this.listContainer = append(container, $('.scm-status.show-file-icons'));
|
||||
|
||||
const updateActionsVisibility = () => toggleClass(this.listContainer, 'show-actions', this.configurationService.getValue<boolean>('scm.alwaysShowActions'));
|
||||
Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'))(updateActionsVisibility);
|
||||
updateActionsVisibility();
|
||||
|
||||
const delegate = new ProviderListDelegate();
|
||||
|
||||
const actionViewItemProvider = (action: IAction) => this.getActionViewItem(action);
|
||||
|
||||
this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility });
|
||||
this._register(this.listLabels);
|
||||
|
||||
const renderers = [
|
||||
new ResourceGroupRenderer(actionViewItemProvider, this.themeService, this.menus),
|
||||
new ResourceRenderer(() => this.viewModel, this.listLabels, actionViewItemProvider, () => this.getSelectedResources(), this.themeService, this.menus)
|
||||
];
|
||||
|
||||
const filter = new SCMTreeFilter();
|
||||
const sorter = new SCMTreeSorter(() => this.viewModel);
|
||||
const keyboardNavigationLabelProvider = new SCMTreeKeyboardNavigationLabelProvider();
|
||||
const identityProvider = new SCMResourceIdentityProvider();
|
||||
|
||||
this.tree = this.instantiationService.createInstance(
|
||||
WorkbenchCompressibleObjectTree,
|
||||
'SCM Tree Repo',
|
||||
this.listContainer,
|
||||
delegate,
|
||||
renderers,
|
||||
{
|
||||
identityProvider,
|
||||
horizontalScrolling: false,
|
||||
filter,
|
||||
sorter,
|
||||
keyboardNavigationLabelProvider
|
||||
});
|
||||
|
||||
this._register(Event.chain(this.tree.onDidOpen)
|
||||
.map(e => e.elements[0])
|
||||
.filter(e => !!e && !ResourceTree.isBranchNode(e) && isSCMResource(e))
|
||||
.on(this.open, this));
|
||||
|
||||
this._register(Event.chain(this.tree.onDidPin)
|
||||
.map(e => e.elements[0])
|
||||
.filter(e => !!e && !ResourceTree.isBranchNode(e) && isSCMResource(e))
|
||||
.on(this.pin, this));
|
||||
|
||||
this._register(this.tree.onContextMenu(this.onListContextMenu, this));
|
||||
this._register(this.tree);
|
||||
|
||||
this.viewModel = new ViewModel(this.repository.provider.groups, this.tree);
|
||||
this._register(this.viewModel);
|
||||
|
||||
addClass(this.listContainer, 'file-icon-themable-tree');
|
||||
addClass(this.listContainer, 'show-file-icons');
|
||||
|
||||
const updateIndentStyles = (theme: IFileIconTheme) => {
|
||||
toggleClass(this.listContainer, 'list-view-mode', this.viewModel.mode === ViewModelMode.List);
|
||||
toggleClass(this.listContainer, 'align-icons-and-twisties', this.viewModel.mode === ViewModelMode.Tree && theme.hasFileIcons && !theme.hasFolderIcons);
|
||||
toggleClass(this.listContainer, 'hide-arrows', this.viewModel.mode === ViewModelMode.Tree && theme.hidesExplorerArrows === true);
|
||||
};
|
||||
|
||||
updateIndentStyles(this.themeService.getFileIconTheme());
|
||||
this._register(this.themeService.onDidFileIconThemeChange(updateIndentStyles));
|
||||
this._register(this.viewModel.onDidChangeMode(() => updateIndentStyles(this.themeService.getFileIconTheme())));
|
||||
|
||||
this.toggleViewModelModeAction = new ToggleViewModeAction(this.viewModel);
|
||||
this._register(this.toggleViewModelModeAction);
|
||||
|
||||
this._register(this.onDidChangeBodyVisibility(this._onDidChangeVisibility, this));
|
||||
|
||||
this.updateActions();
|
||||
}
|
||||
|
||||
layoutBody(height: number | undefined = this.cachedHeight, width: number | undefined = this.cachedWidth): void {
|
||||
if (height === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cachedHeight = height;
|
||||
|
||||
if (this.repository.input.visible) {
|
||||
removeClass(this.inputBoxContainer, 'hidden');
|
||||
this.inputBox.layout();
|
||||
|
||||
const editorHeight = this.inputBox.height;
|
||||
const listHeight = height - (editorHeight + 12 /* margin */);
|
||||
this.listContainer.style.height = `${listHeight}px`;
|
||||
this.tree.layout(listHeight, width);
|
||||
} else {
|
||||
addClass(this.inputBoxContainer, 'hidden');
|
||||
|
||||
this.listContainer.style.height = `${height}px`;
|
||||
this.tree.layout(height, width);
|
||||
}
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
super.focus();
|
||||
|
||||
if (this.isExpanded()) {
|
||||
if (this.repository.input.visible) {
|
||||
this.inputBox.focus();
|
||||
} else {
|
||||
this.tree.domFocus();
|
||||
}
|
||||
|
||||
this.repository.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _onDidChangeVisibility(visible: boolean): void {
|
||||
this.inputBox.setEnabled(visible);
|
||||
this.viewModel.setVisible(visible);
|
||||
}
|
||||
|
||||
getActions(): IAction[] {
|
||||
if (this.toggleViewModelModeAction) {
|
||||
|
||||
return [
|
||||
this.toggleViewModelModeAction,
|
||||
...this.menus.getTitleActions()
|
||||
];
|
||||
} else {
|
||||
return this.menus.getTitleActions();
|
||||
}
|
||||
}
|
||||
|
||||
getSecondaryActions(): IAction[] {
|
||||
return this.menus.getTitleSecondaryActions();
|
||||
}
|
||||
|
||||
getActionViewItem(action: IAction): IActionViewItem | undefined {
|
||||
if (!(action instanceof MenuItemAction)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService);
|
||||
}
|
||||
|
||||
getActionsContext(): any {
|
||||
return this.repository.provider;
|
||||
}
|
||||
|
||||
private open(e: ISCMResource): void {
|
||||
e.open();
|
||||
}
|
||||
|
||||
private pin(): void {
|
||||
const activeControl = this.editorService.activeControl;
|
||||
|
||||
if (activeControl) {
|
||||
activeControl.group.pinEditor(activeControl.input);
|
||||
}
|
||||
}
|
||||
|
||||
private onListContextMenu(e: ITreeContextMenuEvent<TreeElement>): void {
|
||||
if (!e.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = e.element;
|
||||
let actions: IAction[] = [];
|
||||
|
||||
if (ResourceTree.isBranchNode(element)) {
|
||||
actions = this.menus.getResourceFolderContextActions(element.context);
|
||||
} else if (isSCMResource(element)) {
|
||||
actions = this.menus.getResourceContextActions(element);
|
||||
} else {
|
||||
actions = this.menus.getResourceGroupContextActions(element);
|
||||
}
|
||||
|
||||
this.contextMenuService.showContextMenu({
|
||||
getAnchor: () => e.anchor,
|
||||
getActions: () => actions,
|
||||
getActionsContext: () => element,
|
||||
actionRunner: new MultipleSelectionActionRunner(() => this.getSelectedResources())
|
||||
});
|
||||
}
|
||||
|
||||
private getSelectedResources(): (ISCMResource | IBranchNode<ISCMResource, ISCMResourceGroup>)[] {
|
||||
return this.tree.getSelection()
|
||||
.filter(r => !!r && !isSCMResourceGroup(r))! as any;
|
||||
}
|
||||
|
||||
private updateInputBox(): void {
|
||||
if (typeof this.repository.provider.commitTemplate === 'undefined' || !this.repository.input.visible || this.inputBox.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.inputBox.value = this.repository.provider.commitTemplate;
|
||||
}
|
||||
|
||||
private updateInputBoxVisibility(): void {
|
||||
if (this.cachedHeight) {
|
||||
this.layoutBody(this.cachedHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RepositoryViewDescriptor implements IViewDescriptor {
|
||||
|
||||
private static counter = 0;
|
||||
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly ctorDescriptor: { ctor: any, arguments?: any[] };
|
||||
readonly canToggleVisibility = true;
|
||||
readonly order = -500;
|
||||
readonly workspace = true;
|
||||
|
||||
constructor(readonly repository: ISCMRepository, readonly hideByDefault: boolean) {
|
||||
const repoId = repository.provider.rootUri ? repository.provider.rootUri.toString() : `#${RepositoryViewDescriptor.counter++}`;
|
||||
this.id = `scm:repository:${repository.provider.label}:${repoId}`;
|
||||
this.name = repository.provider.rootUri ? basename(repository.provider.rootUri) : repository.provider.label;
|
||||
|
||||
this.ctorDescriptor = { ctor: RepositoryPanel, arguments: [repository] };
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } fro
|
||||
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { SCMStatusController } from './scmActivity';
|
||||
import { SCMStatusController } from './activity';
|
||||
import { SCMViewlet } from 'vs/workbench/contrib/scm/browser/scmViewlet';
|
||||
import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ISCMResourceGroup, ISCMResource } from 'vs/workbench/contrib/scm/common/scm';
|
||||
|
||||
export function isSCMResource(element: ISCMResourceGroup | ISCMResource): element is ISCMResource {
|
||||
return !!(element as ISCMResource).sourceUri;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
53
src/vs/workbench/contrib/scm/browser/util.ts
Normal file
53
src/vs/workbench/contrib/scm/browser/util.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ISCMResource, ISCMRepository, ISCMResourceGroup } from 'vs/workbench/contrib/scm/common/scm';
|
||||
import { IMenu } from 'vs/platform/actions/common/actions';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IDisposable, Disposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
|
||||
export function isSCMRepository(element: any): element is ISCMRepository {
|
||||
return !!(element as ISCMRepository).provider && typeof (element as ISCMRepository).setSelected === 'function';
|
||||
}
|
||||
|
||||
export function isSCMResourceGroup(element: any): element is ISCMResourceGroup {
|
||||
return !!(element as ISCMResourceGroup).provider && !!(element as ISCMResourceGroup).elements;
|
||||
}
|
||||
|
||||
export function isSCMResource(element: any): element is ISCMResource {
|
||||
return !!(element as ISCMResource).sourceUri && isSCMResourceGroup((element as ISCMResource).resourceGroup);
|
||||
}
|
||||
|
||||
export function connectPrimaryMenuToInlineActionBar(menu: IMenu, actionBar: ActionBar): IDisposable {
|
||||
let cachedDisposable: IDisposable = Disposable.None;
|
||||
let cachedPrimary: IAction[] = [];
|
||||
|
||||
const updateActions = () => {
|
||||
const primary: IAction[] = [];
|
||||
const secondary: IAction[] = [];
|
||||
|
||||
const disposable = createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, { primary, secondary }, g => /^inline/.test(g));
|
||||
|
||||
if (equals(cachedPrimary, primary, (a, b) => a.id === b.id)) {
|
||||
disposable.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
cachedDisposable = disposable;
|
||||
cachedPrimary = primary;
|
||||
|
||||
actionBar.clear();
|
||||
actionBar.push(primary, { icon: true, label: false });
|
||||
};
|
||||
|
||||
updateActions();
|
||||
|
||||
return combinedDisposable(menu.onDidChange(updateActions), toDisposable(() => {
|
||||
cachedDisposable.dispose();
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user