Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

View/Sort feature for SCM viewlet #99768

Merged
merged 3 commits into from Jun 11, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
187 changes: 154 additions & 33 deletions src/vs/workbench/contrib/scm/browser/repositoryPane.ts
Expand Up @@ -20,10 +20,10 @@ 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 { IAction, IActionViewItem, ActionRunner, Action, RadioGroup } 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 { ActionBar, IActionViewItemProvider, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
import { IThemeService, LIGHT, registerThemingParticipant, IFileIconTheme } from 'vs/platform/theme/common/themeService';
import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar } from './util';
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
Expand All @@ -39,7 +39,7 @@ import { Iterable } 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 { compareFileNames, comparePaths } from 'vs/base/common/comparers';
import { FuzzyScore, createMatches, IMatch } from 'vs/base/common/filters';
import { IViewDescriptor, IViewDescriptorService } from 'vs/workbench/common/views';
import { localize } from 'vs/nls';
Expand All @@ -60,7 +60,7 @@ import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEdito
import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import * as platform from 'vs/base/common/platform';
import { format } from 'vs/base/common/strings';
import { format, compare } from 'vs/base/common/strings';
import { inputPlaceholderForeground, inputValidationInfoBorder, inputValidationWarningBorder, inputValidationErrorBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBackground, inputValidationErrorForeground, inputBackground, inputForeground, inputBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
Expand All @@ -74,6 +74,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { IModeService } from 'vs/editor/common/services/modeService';
import { ILabelService } from 'vs/platform/label/common/label';
import { ContextSubMenu } from 'vs/base/browser/contextmenu';
import { KeyCode } from 'vs/base/common/keyCodes';
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';

Expand Down Expand Up @@ -373,14 +374,38 @@ export class SCMTreeSorter implements ITreeSorter<TreeElement> {
constructor(private viewModelProvider: () => ViewModel) { }

compare(one: TreeElement, other: TreeElement): number {
if (this.viewModel.mode === ViewModelMode.List) {
if (isSCMResourceGroup(one) && isSCMResourceGroup(other)) {
return 0;
}

if (isSCMResourceGroup(one) && isSCMResourceGroup(other)) {
return 0;
// List
if (this.viewModel.mode === ViewModelMode.List) {
// FileName
if (this.viewModel.sortKey === ViewModelSortKey.Name) {
const oneName = basename((one as ISCMResource).sourceUri);
const otherName = basename((other as ISCMResource).sourceUri);

return compareFileNames(oneName, otherName);
}

// Status
if (this.viewModel.sortKey === ViewModelSortKey.Status) {
const oneTooltip = (one as ISCMResource).decorations.tooltip ?? '';
const otherTooltip = (other as ISCMResource).decorations.tooltip ?? '';

if (oneTooltip !== otherTooltip) {
return compare(oneTooltip, otherTooltip);
}
}

// Path (default)
const onePath = (one as ISCMResource).sourceUri.fsPath;
const otherPath = (other as ISCMResource).sourceUri.fsPath;

return comparePaths(onePath, otherPath);
}

// Tree
const oneIsDirectory = ResourceTree.isResourceNode(one);
const otherIsDirectory = ResourceTree.isResourceNode(other);

Expand Down Expand Up @@ -498,6 +523,12 @@ const enum ViewModelMode {
Tree = 'tree'
}

const enum ViewModelSortKey {
Path,
Name,
Status
}

class ViewModel {

private readonly _onDidChangeMode = new Emitter<ViewModelMode>();
Expand All @@ -521,6 +552,14 @@ class ViewModel {
this._onDidChangeMode.fire(mode);
}

get sortKey(): ViewModelSortKey { return this._sortKey; }
set sortKey(sortKey: ViewModelSortKey) {
if (sortKey !== this._sortKey) {
this._sortKey = sortKey;
this.refresh();
}
}

private items: IGroupItem[] = [];
private visibilityDisposables = new DisposableStore();
private scrollTop: number | undefined;
Expand All @@ -531,6 +570,7 @@ class ViewModel {
private groups: ISequence<ISCMResourceGroup>,
private tree: WorkbenchCompressibleObjectTree<TreeElement, FuzzyScore>,
private _mode: ViewModelMode,
private _sortKey: ViewModelSortKey,
@IEditorService protected editorService: IEditorService,
@IConfigurationService protected configurationService: IConfigurationService,
) { }
Expand Down Expand Up @@ -661,25 +701,107 @@ class ViewModel {
}
}

export class ToggleViewModeAction extends Action {
class SCMViewSubMenuAction extends ContextSubMenu {
constructor(viewModel: ViewModel) {
super(localize('sortAction', "View & Sort"),
[
...new RadioGroup([
new SCMViewModeListAction(viewModel),
new SCMViewModeTreeAction(viewModel)
]).actions,
new Separator(),
...new RadioGroup([
new SCMSortByNameAction(viewModel),
new SCMSortByPathAction(viewModel),
new SCMSortByStatusAction(viewModel)
]).actions
]
);
}
}

static readonly ID = 'workbench.scm.action.toggleViewMode';
static readonly LABEL = localize('toggleViewMode', "Toggle View Mode");
abstract class SCMViewModeAction extends Action {
constructor(id: string, label: string, private viewModel: ViewModel, private viewMode: ViewModelMode) {
super(id, label);

constructor(private viewModel: ViewModel) {
super(ToggleViewModeAction.ID, ToggleViewModeAction.LABEL);
this.checked = this.viewModel.mode === this.viewMode;
}

this._register(this.viewModel.onDidChangeMode(this.onDidChangeMode, this));
this.onDidChangeMode(this.viewModel.mode);
async run(): Promise<void> {
if (this.viewMode !== this.viewModel.mode) {
this.checked = !this.checked;
this.viewModel.mode = this.viewMode;
}
}
}

class SCMViewModeListAction extends SCMViewModeAction {
static readonly ID = 'workbench.scm.action.viewModeList';
static readonly LABEL = localize('viewModeList', "View as List");

constructor(viewModel: ViewModel) {
super(SCMViewModeListAction.ID, SCMViewModeListAction.LABEL, viewModel, ViewModelMode.List);
}
}

class SCMViewModeTreeAction extends SCMViewModeAction {
static readonly ID = 'workbench.scm.action.viewModeTree';
static readonly LABEL = localize('viewModeTree', "View as Tree");

constructor(viewModel: ViewModel) {
super(SCMViewModeTreeAction.ID, SCMViewModeTreeAction.LABEL, viewModel, ViewModelMode.Tree);
}
}

abstract class SCMSortAction extends Action {

private readonly _listener: IDisposable;

constructor(id: string, label: string, private viewModel: ViewModel, private sortKey: ViewModelSortKey) {
super(id, label);

this.checked = this.sortKey === ViewModelSortKey.Path;
this.enabled = this.viewModel?.mode === ViewModelMode.List ?? false;
this._listener = viewModel?.onDidChangeMode(e => this.enabled = e === ViewModelMode.List);
}

async run(): Promise<void> {
this.viewModel.mode = this.viewModel.mode === ViewModelMode.List ? ViewModelMode.Tree : ViewModelMode.List;
if (this.sortKey !== this.viewModel.sortKey) {
this.checked = !this.checked;
this.viewModel.sortKey = this.sortKey;
}
}

dispose(): void {
this._listener.dispose();
super.dispose();
}
}

class SCMSortByNameAction extends SCMSortAction {
static readonly ID = 'workbench.scm.action.sortByName';
static readonly LABEL = localize('sortByName', "Sort by Name");

private onDidChangeMode(mode: ViewModelMode): void {
const iconClass = mode === ViewModelMode.List ? 'codicon-list-tree' : 'codicon-list-flat';
this.class = `scm-action toggle-view-mode ${iconClass}`;
constructor(viewModel: ViewModel) {
super(SCMSortByNameAction.ID, SCMSortByNameAction.LABEL, viewModel, ViewModelSortKey.Name);
}
}

class SCMSortByPathAction extends SCMSortAction {
static readonly ID = 'workbench.scm.action.sortByPath';
static readonly LABEL = localize('sortByPath', "Sort by Path");

constructor(viewModel: ViewModel) {
super(SCMSortByPathAction.ID, SCMSortByPathAction.LABEL, viewModel, ViewModelSortKey.Path);
}
}

class SCMSortByStatusAction extends SCMSortAction {
static readonly ID = 'workbench.scm.action.sortByStatus';
static readonly LABEL = localize('sortByStatus', "Sort by Status");

constructor(viewModel: ViewModel) {
super(SCMSortByStatusAction.ID, SCMSortByStatusAction.LABEL, viewModel, ViewModelSortKey.Status);
}
}

Expand All @@ -697,7 +819,6 @@ export class RepositoryPane extends ViewPane {
private viewModel!: ViewModel;
private listLabels!: ResourceLabels;
private menus: SCMMenus;
private toggleViewModelModeAction: ToggleViewModeAction | undefined;
protected contextKeyService: IContextKeyService;
private commitTemplate = '';

Expand Down Expand Up @@ -971,7 +1092,7 @@ export class RepositoryPane extends ViewPane {
}
}

this.viewModel = this.instantiationService.createInstance(ViewModel, this.repository.provider.groups, this.tree, viewMode);
this.viewModel = this.instantiationService.createInstance(ViewModel, this.repository.provider.groups, this.tree, viewMode, ViewModelSortKey.Path);
this._register(this.viewModel);

addClass(this.listContainer, 'file-icon-themable-tree');
Expand All @@ -981,9 +1102,6 @@ export class RepositoryPane extends ViewPane {
this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this));
this._register(this.viewModel.onDidChangeMode(this.onDidChangeMode, this));

this.toggleViewModelModeAction = new ToggleViewModeAction(this.viewModel);
this._register(this.toggleViewModelModeAction);

this._register(this.onDidChangeBodyVisibility(this._onDidChangeVisibility, this));

this.updateActions();
Expand Down Expand Up @@ -1066,19 +1184,22 @@ export class RepositoryPane extends ViewPane {
}

getActions(): IAction[] {
if (this.toggleViewModelModeAction) {

return [
this.toggleViewModelModeAction,
...this.menus.getTitleActions()
];
} else {
return this.menus.getTitleActions();
}
return this.menus.getTitleActions();
}

getSecondaryActions(): IAction[] {
return this.menus.getTitleSecondaryActions();
if (!this.viewModel) {
return [];
}

const result: IAction[] = [new SCMViewSubMenuAction(this.viewModel)];
const secondaryActions = this.menus.getTitleSecondaryActions();

if (secondaryActions.length > 0) {
result.push(new Separator(), ...secondaryActions);
}

return result;
}

getActionViewItem(action: IAction): IActionViewItem | undefined {
Expand Down