Skip to content

Commit c630abd

Browse files
authored
View/Sort feature for SCM viewlet (microsoft#99768)
* View/Sort feature for SCM viewlet
1 parent 477701a commit c630abd

1 file changed

Lines changed: 154 additions & 33 deletions

File tree

src/vs/workbench/contrib/scm/browser/repositoryPane.ts

Lines changed: 154 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
2020
import { ICommandService } from 'vs/platform/commands/common/commands';
2121
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
2222
import { MenuItemAction, IMenuService } from 'vs/platform/actions/common/actions';
23-
import { IAction, IActionViewItem, ActionRunner, Action } from 'vs/base/common/actions';
23+
import { IAction, IActionViewItem, ActionRunner, Action, RadioGroup } from 'vs/base/common/actions';
2424
import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
2525
import { SCMMenus } from './menus';
26-
import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
26+
import { ActionBar, IActionViewItemProvider, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
2727
import { IThemeService, LIGHT, registerThemingParticipant, IFileIconTheme } from 'vs/platform/theme/common/themeService';
2828
import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar } from './util';
2929
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
@@ -39,7 +39,7 @@ import { Iterable } from 'vs/base/common/iterator';
3939
import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
4040
import { URI } from 'vs/base/common/uri';
4141
import { FileKind } from 'vs/platform/files/common/files';
42-
import { compareFileNames } from 'vs/base/common/comparers';
42+
import { compareFileNames, comparePaths } from 'vs/base/common/comparers';
4343
import { FuzzyScore, createMatches, IMatch } from 'vs/base/common/filters';
4444
import { IViewDescriptor, IViewDescriptorService } from 'vs/workbench/common/views';
4545
import { localize } from 'vs/nls';
@@ -60,7 +60,7 @@ import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEdito
6060
import { ContextMenuController } from 'vs/editor/contrib/contextmenu/contextmenu';
6161
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
6262
import * as platform from 'vs/base/common/platform';
63-
import { format } from 'vs/base/common/strings';
63+
import { format, compare } from 'vs/base/common/strings';
6464
import { inputPlaceholderForeground, inputValidationInfoBorder, inputValidationWarningBorder, inputValidationErrorBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBackground, inputValidationErrorForeground, inputBackground, inputForeground, inputBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
6565
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
6666
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
@@ -74,6 +74,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
7474
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
7575
import { IModeService } from 'vs/editor/common/services/modeService';
7676
import { ILabelService } from 'vs/platform/label/common/label';
77+
import { ContextSubMenu } from 'vs/base/browser/contextmenu';
7778
import { KeyCode } from 'vs/base/common/keyCodes';
7879
import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style';
7980

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

375376
compare(one: TreeElement, other: TreeElement): number {
376-
if (this.viewModel.mode === ViewModelMode.List) {
377+
if (isSCMResourceGroup(one) && isSCMResourceGroup(other)) {
377378
return 0;
378379
}
379380

380-
if (isSCMResourceGroup(one) && isSCMResourceGroup(other)) {
381-
return 0;
381+
// List
382+
if (this.viewModel.mode === ViewModelMode.List) {
383+
// FileName
384+
if (this.viewModel.sortKey === ViewModelSortKey.Name) {
385+
const oneName = basename((one as ISCMResource).sourceUri);
386+
const otherName = basename((other as ISCMResource).sourceUri);
387+
388+
return compareFileNames(oneName, otherName);
389+
}
390+
391+
// Status
392+
if (this.viewModel.sortKey === ViewModelSortKey.Status) {
393+
const oneTooltip = (one as ISCMResource).decorations.tooltip ?? '';
394+
const otherTooltip = (other as ISCMResource).decorations.tooltip ?? '';
395+
396+
if (oneTooltip !== otherTooltip) {
397+
return compare(oneTooltip, otherTooltip);
398+
}
399+
}
400+
401+
// Path (default)
402+
const onePath = (one as ISCMResource).sourceUri.fsPath;
403+
const otherPath = (other as ISCMResource).sourceUri.fsPath;
404+
405+
return comparePaths(onePath, otherPath);
382406
}
383407

408+
// Tree
384409
const oneIsDirectory = ResourceTree.isResourceNode(one);
385410
const otherIsDirectory = ResourceTree.isResourceNode(other);
386411

@@ -498,6 +523,12 @@ const enum ViewModelMode {
498523
Tree = 'tree'
499524
}
500525

526+
const enum ViewModelSortKey {
527+
Path,
528+
Name,
529+
Status
530+
}
531+
501532
class ViewModel {
502533

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

555+
get sortKey(): ViewModelSortKey { return this._sortKey; }
556+
set sortKey(sortKey: ViewModelSortKey) {
557+
if (sortKey !== this._sortKey) {
558+
this._sortKey = sortKey;
559+
this.refresh();
560+
}
561+
}
562+
524563
private items: IGroupItem[] = [];
525564
private visibilityDisposables = new DisposableStore();
526565
private scrollTop: number | undefined;
@@ -531,6 +570,7 @@ class ViewModel {
531570
private groups: ISequence<ISCMResourceGroup>,
532571
private tree: WorkbenchCompressibleObjectTree<TreeElement, FuzzyScore>,
533572
private _mode: ViewModelMode,
573+
private _sortKey: ViewModelSortKey,
534574
@IEditorService protected editorService: IEditorService,
535575
@IConfigurationService protected configurationService: IConfigurationService,
536576
) { }
@@ -661,25 +701,107 @@ class ViewModel {
661701
}
662702
}
663703

664-
export class ToggleViewModeAction extends Action {
704+
class SCMViewSubMenuAction extends ContextSubMenu {
705+
constructor(viewModel: ViewModel) {
706+
super(localize('sortAction', "View & Sort"),
707+
[
708+
...new RadioGroup([
709+
new SCMViewModeListAction(viewModel),
710+
new SCMViewModeTreeAction(viewModel)
711+
]).actions,
712+
new Separator(),
713+
...new RadioGroup([
714+
new SCMSortByNameAction(viewModel),
715+
new SCMSortByPathAction(viewModel),
716+
new SCMSortByStatusAction(viewModel)
717+
]).actions
718+
]
719+
);
720+
}
721+
}
665722

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

669-
constructor(private viewModel: ViewModel) {
670-
super(ToggleViewModeAction.ID, ToggleViewModeAction.LABEL);
727+
this.checked = this.viewModel.mode === this.viewMode;
728+
}
671729

672-
this._register(this.viewModel.onDidChangeMode(this.onDidChangeMode, this));
673-
this.onDidChangeMode(this.viewModel.mode);
730+
async run(): Promise<void> {
731+
if (this.viewMode !== this.viewModel.mode) {
732+
this.checked = !this.checked;
733+
this.viewModel.mode = this.viewMode;
734+
}
735+
}
736+
}
737+
738+
class SCMViewModeListAction extends SCMViewModeAction {
739+
static readonly ID = 'workbench.scm.action.viewModeList';
740+
static readonly LABEL = localize('viewModeList', "View as List");
741+
742+
constructor(viewModel: ViewModel) {
743+
super(SCMViewModeListAction.ID, SCMViewModeListAction.LABEL, viewModel, ViewModelMode.List);
744+
}
745+
}
746+
747+
class SCMViewModeTreeAction extends SCMViewModeAction {
748+
static readonly ID = 'workbench.scm.action.viewModeTree';
749+
static readonly LABEL = localize('viewModeTree', "View as Tree");
750+
751+
constructor(viewModel: ViewModel) {
752+
super(SCMViewModeTreeAction.ID, SCMViewModeTreeAction.LABEL, viewModel, ViewModelMode.Tree);
753+
}
754+
}
755+
756+
abstract class SCMSortAction extends Action {
757+
758+
private readonly _listener: IDisposable;
759+
760+
constructor(id: string, label: string, private viewModel: ViewModel, private sortKey: ViewModelSortKey) {
761+
super(id, label);
762+
763+
this.checked = this.sortKey === ViewModelSortKey.Path;
764+
this.enabled = this.viewModel?.mode === ViewModelMode.List ?? false;
765+
this._listener = viewModel?.onDidChangeMode(e => this.enabled = e === ViewModelMode.List);
674766
}
675767

676768
async run(): Promise<void> {
677-
this.viewModel.mode = this.viewModel.mode === ViewModelMode.List ? ViewModelMode.Tree : ViewModelMode.List;
769+
if (this.sortKey !== this.viewModel.sortKey) {
770+
this.checked = !this.checked;
771+
this.viewModel.sortKey = this.sortKey;
772+
}
773+
}
774+
775+
dispose(): void {
776+
this._listener.dispose();
777+
super.dispose();
678778
}
779+
}
780+
781+
class SCMSortByNameAction extends SCMSortAction {
782+
static readonly ID = 'workbench.scm.action.sortByName';
783+
static readonly LABEL = localize('sortByName', "Sort by Name");
679784

680-
private onDidChangeMode(mode: ViewModelMode): void {
681-
const iconClass = mode === ViewModelMode.List ? 'codicon-list-tree' : 'codicon-list-flat';
682-
this.class = `scm-action toggle-view-mode ${iconClass}`;
785+
constructor(viewModel: ViewModel) {
786+
super(SCMSortByNameAction.ID, SCMSortByNameAction.LABEL, viewModel, ViewModelSortKey.Name);
787+
}
788+
}
789+
790+
class SCMSortByPathAction extends SCMSortAction {
791+
static readonly ID = 'workbench.scm.action.sortByPath';
792+
static readonly LABEL = localize('sortByPath', "Sort by Path");
793+
794+
constructor(viewModel: ViewModel) {
795+
super(SCMSortByPathAction.ID, SCMSortByPathAction.LABEL, viewModel, ViewModelSortKey.Path);
796+
}
797+
}
798+
799+
class SCMSortByStatusAction extends SCMSortAction {
800+
static readonly ID = 'workbench.scm.action.sortByStatus';
801+
static readonly LABEL = localize('sortByStatus', "Sort by Status");
802+
803+
constructor(viewModel: ViewModel) {
804+
super(SCMSortByStatusAction.ID, SCMSortByStatusAction.LABEL, viewModel, ViewModelSortKey.Status);
683805
}
684806
}
685807

@@ -697,7 +819,6 @@ export class RepositoryPane extends ViewPane {
697819
private viewModel!: ViewModel;
698820
private listLabels!: ResourceLabels;
699821
private menus: SCMMenus;
700-
private toggleViewModelModeAction: ToggleViewModeAction | undefined;
701822
protected contextKeyService: IContextKeyService;
702823
private commitTemplate = '';
703824

@@ -971,7 +1092,7 @@ export class RepositoryPane extends ViewPane {
9711092
}
9721093
}
9731094

974-
this.viewModel = this.instantiationService.createInstance(ViewModel, this.repository.provider.groups, this.tree, viewMode);
1095+
this.viewModel = this.instantiationService.createInstance(ViewModel, this.repository.provider.groups, this.tree, viewMode, ViewModelSortKey.Path);
9751096
this._register(this.viewModel);
9761097

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

984-
this.toggleViewModelModeAction = new ToggleViewModeAction(this.viewModel);
985-
this._register(this.toggleViewModelModeAction);
986-
9871105
this._register(this.onDidChangeBodyVisibility(this._onDidChangeVisibility, this));
9881106

9891107
this.updateActions();
@@ -1066,19 +1184,22 @@ export class RepositoryPane extends ViewPane {
10661184
}
10671185

10681186
getActions(): IAction[] {
1069-
if (this.toggleViewModelModeAction) {
1070-
1071-
return [
1072-
this.toggleViewModelModeAction,
1073-
...this.menus.getTitleActions()
1074-
];
1075-
} else {
1076-
return this.menus.getTitleActions();
1077-
}
1187+
return this.menus.getTitleActions();
10781188
}
10791189

10801190
getSecondaryActions(): IAction[] {
1081-
return this.menus.getTitleSecondaryActions();
1191+
if (!this.viewModel) {
1192+
return [];
1193+
}
1194+
1195+
const result: IAction[] = [new SCMViewSubMenuAction(this.viewModel)];
1196+
const secondaryActions = this.menus.getTitleSecondaryActions();
1197+
1198+
if (secondaryActions.length > 0) {
1199+
result.push(new Separator(), ...secondaryActions);
1200+
}
1201+
1202+
return result;
10821203
}
10831204

10841205
getActionViewItem(action: IAction): IActionViewItem | undefined {

0 commit comments

Comments
 (0)