Skip to content

Commit af6adc6

Browse files
authored
SCM - introduce single selection mode for repositories view (#271831)
* Initial implementation * Pinning/unpinning selection is working * 💄 fix hygiene
1 parent af1cbea commit af6adc6

File tree

4 files changed

+147
-21
lines changed

4 files changed

+147
-21
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,16 @@ Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).regis
348348
description: localize('providersVisible', "Controls how many repositories are visible in the Source Control Repositories section. Set to 0, to be able to manually resize the view."),
349349
default: 10
350350
},
351+
'scm.repositories.selectionMode': {
352+
type: 'string',
353+
enum: ['multiple', 'single'],
354+
enumDescriptions: [
355+
localize('scm.repositories.selectionMode.multiple', "Multiple repositories can be selected at the same time."),
356+
localize('scm.repositories.selectionMode.single', "Only one repository can be selected at a time.")
357+
],
358+
description: localize('scm.repositories.selectionMode', "Controls the selection mode of the repositories in the Source Control Repositories view."),
359+
default: 'multiple'
360+
},
351361
'scm.showActionButton': {
352362
type: 'boolean',
353363
markdownDescription: localize('showActionButton', "Controls whether an action button can be shown in the Source Control view."),
@@ -549,7 +559,7 @@ CommandsRegistry.registerCommand('scm.setActiveProvider', async (accessor) => {
549559
const scmViewService = accessor.get(ISCMViewService);
550560

551561
const placeHolder = localize('scmActiveRepositoryPlaceHolder', "Select the active repository, type to filter all repositories");
552-
const autoQuickItemDescription = localize('scmActiveRepositoryAutoDescription', "The active repository is updated based on focused repository/active editor");
562+
const autoQuickItemDescription = localize('scmActiveRepositoryAutoDescription', "The active repository is updated based on active editor");
553563
const repositoryPicker = instantiationService.createInstance(RepositoryPicker, placeHolder, autoQuickItemDescription);
554564

555565
const result = await repositoryPicker.pickRepository();

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,10 @@ registerAction2(class extends ViewAction<SCMHistoryViewPane> {
169169
f1: false,
170170
menu: {
171171
id: MenuId.SCMHistoryTitle,
172-
when: ContextKeyExpr.and(ContextKeyExpr.has('scm.providerCount'), ContextKeyExpr.greater('scm.providerCount', 1)),
172+
when: ContextKeyExpr.and(
173+
ContextKeyExpr.has('scm.providerCount'),
174+
ContextKeyExpr.greater('scm.providerCount', 1),
175+
ContextKeyExpr.equals('config.scm.repositories.selectionMode', 'multiple')),
173176
group: 'navigation',
174177
order: 0
175178
}

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

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { IListVirtualDelegate, IIdentityProvider } from '../../../../base/browse
1111
import { IAsyncDataSource, ITreeEvent, ITreeContextMenuEvent } from '../../../../base/browser/ui/tree/tree.js';
1212
import { WorkbenchCompressibleAsyncDataTree } from '../../../../platform/list/browser/listService.js';
1313
import { ISCMRepository, ISCMService, ISCMViewService } from '../common/scm.js';
14-
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
14+
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
1515
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
1616
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
1717
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
@@ -24,11 +24,13 @@ import { RepositoryActionRunner, RepositoryRenderer } from './scmRepositoryRende
2424
import { collectContextMenuActions, getActionViewItemProvider, isSCMRepository } from './util.js';
2525
import { Orientation } from '../../../../base/browser/ui/sash/sash.js';
2626
import { Iterable } from '../../../../base/common/iterator.js';
27-
import { MenuId } from '../../../../platform/actions/common/actions.js';
27+
import { MenuId, registerAction2, Action2 } from '../../../../platform/actions/common/actions.js';
2828
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
2929
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
3030
import { autorun, IObservable, observableFromEvent, observableSignalFromEvent } from '../../../../base/common/observable.js';
3131
import { Sequencer } from '../../../../base/common/async.js';
32+
import { Codicon } from '../../../../base/common/codicons.js';
33+
import { RepositoryContextKeys } from './scmViewService.js';
3234

3335
class ListDelegate implements IListVirtualDelegate<ISCMRepository> {
3436

@@ -190,6 +192,7 @@ export class SCMRepositoriesViewPane extends ViewPane {
190192
this._register(this.treeDataSource);
191193

192194
const compressionEnabled = observableConfigValue('scm.compactFolders', true, this.configurationService);
195+
const selectionModeConfig = observableConfigValue<'multiple' | 'single'>('scm.repositories.selectionMode', 'multiple', this.configurationService);
193196

194197
this.tree = this.instantiationService.createInstance(
195198
WorkbenchCompressibleAsyncDataTree,
@@ -214,6 +217,7 @@ export class SCMRepositoriesViewPane extends ViewPane {
214217
},
215218
compressionEnabled: compressionEnabled.get(),
216219
overrideStyles: this.getLocationBasedColors().listOverrideStyles,
220+
multipleSelectionSupport: selectionModeConfig.get() === 'multiple',
217221
expandOnDoubleClick: false,
218222
expandOnlyOnTwistieClick: true,
219223
accessibilityProvider: {
@@ -228,6 +232,11 @@ export class SCMRepositoriesViewPane extends ViewPane {
228232
) as WorkbenchCompressibleAsyncDataTree<ISCMViewService, ISCMRepository, any>;
229233
this._register(this.tree);
230234

235+
this._register(autorun(reader => {
236+
const selectionMode = selectionModeConfig.read(reader);
237+
this.tree.updateOptions({ multipleSelectionSupport: selectionMode === 'multiple' });
238+
}));
239+
231240
this._register(this.tree.onDidChangeSelection(this.onTreeSelectionChange, this));
232241
this._register(this.tree.onDidChangeFocus(this.onTreeDidChangeFocus, this));
233242
this._register(this.tree.onDidFocus(this.onDidTreeFocus, this));
@@ -372,3 +381,53 @@ export class SCMRepositoriesViewPane extends ViewPane {
372381
super.dispose();
373382
}
374383
}
384+
385+
registerAction2(class extends Action2 {
386+
constructor() {
387+
super({
388+
id: 'scm.repositories.pinSelection',
389+
title: localize('scmPinSelection', "Pin the Current Selection"),
390+
f1: false,
391+
icon: Codicon.pin,
392+
menu: {
393+
id: MenuId.SCMSourceControlTitle,
394+
when: RepositoryContextKeys.RepositoryPinned.isEqualTo(false),
395+
group: 'navigation',
396+
order: 1
397+
},
398+
});
399+
}
400+
401+
override async run(accessor: ServicesAccessor): Promise<void> {
402+
const scmViewService = accessor.get(ISCMViewService);
403+
const activeRepository = scmViewService.activeRepository.get();
404+
if (!activeRepository) {
405+
return;
406+
}
407+
408+
scmViewService.pinActiveRepository(activeRepository);
409+
}
410+
});
411+
412+
registerAction2(class extends Action2 {
413+
constructor() {
414+
super({
415+
id: 'scm.repositories.unpinSelection',
416+
title: localize('scmUnpinSelection', "Unpin the Current Selection"),
417+
f1: false,
418+
icon: Codicon.pinned,
419+
menu: {
420+
id: MenuId.SCMSourceControlTitle,
421+
when: RepositoryContextKeys.RepositoryPinned.isEqualTo(true),
422+
group: 'navigation',
423+
order: 2
424+
},
425+
});
426+
}
427+
428+
override async run(accessor: ServicesAccessor): Promise<void> {
429+
const scmViewService = accessor.get(ISCMViewService);
430+
scmViewService.pinActiveRepository(undefined);
431+
}
432+
});
433+

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

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ import { binarySearch } from '../../../../base/common/arrays.js';
1818
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
1919
import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
2020
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
21-
import { derivedObservableWithCache, derivedOpts, IObservable, ISettableObservable, latestChangedValue, observableFromEventOpts, observableValue } from '../../../../base/common/observable.js';
21+
import { autorun, derivedObservableWithCache, derivedOpts, IObservable, ISettableObservable, latestChangedValue, observableFromEventOpts, observableValue, runOnChange } from '../../../../base/common/observable.js';
2222
import { IEditorService } from '../../../services/editor/common/editorService.js';
2323
import { EditorResourceAccessor } from '../../../common/editor.js';
2424
import { EditorInput } from '../../../common/editor/editorInput.js';
2525
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js';
2626
import { ThemeIcon } from '../../../../base/common/themables.js';
2727
import { Codicon } from '../../../../base/common/codicons.js';
2828
import { localize } from '../../../../nls.js';
29+
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
2930

3031
function getProviderStorageKey(provider: ISCMProvider): string {
3132
return `${provider.providerId}:${provider.label}${provider.rootUri ? `:${provider.rootUri.toString()}` : ''}`;
@@ -42,6 +43,7 @@ function getRepositoryName(workspaceContextService: IWorkspaceContextService, re
4243

4344
export const RepositoryContextKeys = {
4445
RepositorySortKey: new RawContextKey<ISCMRepositorySortKey>('scmRepositorySortKey', ISCMRepositorySortKey.DiscoveryTime),
46+
RepositoryPinned: new RawContextKey<boolean>('scmRepositoryPinned', false)
4547
};
4648

4749
export type RepositoryQuickPickItem = IQuickPickItem & { repository: 'auto' | ISCMRepository };
@@ -204,16 +206,20 @@ export class SCMViewService implements ISCMViewService {
204206
private readonly _activeEditorRepositoryObs: IObservable<ISCMRepository | undefined>;
205207

206208
/**
207-
* The focused repository takes precedence over the active editor repository when the observable
208-
* values are updated in the same transaction (or during the initial read of the observable value).
209+
* The focused repository takes precedence over the active editor repository when the observable
210+
* values are updated in the same transaction (or during the initial read of the observable value).
209211
*/
210212
private readonly _activeRepositoryObs: IObservable<ISCMRepository | undefined>;
211213
private readonly _activeRepositoryPinnedObs: ISettableObservable<ISCMRepository | undefined>;
212214
private readonly _focusedRepositoryObs: IObservable<ISCMRepository | undefined>;
213215

216+
private readonly _selectionModeConfig: IObservable<'multiple' | 'single'>;
217+
214218
private _repositoriesSortKey: ISCMRepositorySortKey;
215219
private _sortKeyContextKey: IContextKey<ISCMRepositorySortKey>;
216220

221+
private _repositoryPinnedContextKey: IContextKey<boolean>;
222+
217223
constructor(
218224
@ISCMService private readonly scmService: ISCMService,
219225
@IContextKeyService contextKeyService: IContextKeyService,
@@ -226,6 +232,8 @@ export class SCMViewService implements ISCMViewService {
226232
) {
227233
this.menus = instantiationService.createInstance(SCMMenus);
228234

235+
this._selectionModeConfig = observableConfigValue<'multiple' | 'single'>('scm.repositories.selectionMode', 'multiple', this.configurationService);
236+
229237
this._focusedRepositoryObs = observableFromEventOpts<ISCMRepository | undefined>(
230238
{
231239
owner: this,
@@ -253,7 +261,7 @@ export class SCMViewService implements ISCMViewService {
253261
return lastValue;
254262
}
255263

256-
return Object.create(repository);
264+
return repository;
257265
});
258266

259267
this._activeRepositoryPinnedObs = observableValue<ISCMRepository | undefined>(this, undefined);
@@ -269,8 +277,33 @@ export class SCMViewService implements ISCMViewService {
269277
return activeRepositoryPinned ?? activeRepository;
270278
});
271279

280+
this.disposables.add(autorun(reader => {
281+
const selectionMode = this._selectionModeConfig.read(undefined);
282+
const activeRepository = this.activeRepository.read(reader);
283+
284+
if (selectionMode === 'single' && activeRepository) {
285+
this.visibleRepositories = [activeRepository];
286+
}
287+
}));
288+
289+
this.disposables.add(runOnChange(this._selectionModeConfig, selectionMode => {
290+
if (selectionMode === 'single' && this.visibleRepositories.length > 1) {
291+
const repository = this.visibleRepositories[0];
292+
this.visibleRepositories = [repository];
293+
}
294+
}));
295+
272296
try {
273297
this.previousState = JSON.parse(storageService.get('scm:view:visibleRepositories', StorageScope.WORKSPACE, ''));
298+
299+
// If previously there were multiple visible repositories but the
300+
// view mode is `single`, only restore the first visible repository.
301+
if (this.previousState && this.previousState.visible.length > 1 && this._selectionModeConfig.get() === 'single') {
302+
this.previousState = {
303+
...this.previousState,
304+
visible: [this.previousState.visible[0]]
305+
};
306+
}
274307
} catch {
275308
// noop
276309
}
@@ -279,6 +312,9 @@ export class SCMViewService implements ISCMViewService {
279312
this._sortKeyContextKey = RepositoryContextKeys.RepositorySortKey.bindTo(contextKeyService);
280313
this._sortKeyContextKey.set(this._repositoriesSortKey);
281314

315+
this._repositoryPinnedContextKey = RepositoryContextKeys.RepositoryPinned.bindTo(contextKeyService);
316+
this._repositoryPinnedContextKey.set(!!this._activeRepositoryPinnedObs.get());
317+
282318
scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables);
283319
scmService.onDidRemoveRepository(this.onDidRemoveRepository, this, this.disposables);
284320

@@ -314,19 +350,24 @@ export class SCMViewService implements ISCMViewService {
314350
if (index === -1) {
315351
// This repository is not part of the previous state which means that it
316352
// was either manually closed in the previous session, or the repository
317-
// was added after the previous session.In this case, we should select all
318-
// of the repositories.
353+
// was added after the previous session. In this case, we should select
354+
// all of the repositories.
319355
const added: ISCMRepository[] = [];
320356

321357
this.insertRepositoryView(this._repositories, repositoryView);
322-
this._repositories.forEach((repositoryView, index) => {
323-
if (repositoryView.selectionIndex === -1) {
324-
added.push(repositoryView.repository);
325-
}
326-
repositoryView.selectionIndex = index;
327-
});
328358

329-
this._onDidChangeRepositories.fire({ added, removed: Iterable.empty() });
359+
if (this._selectionModeConfig.get() === 'multiple' || !this._repositories.find(r => r.selectionIndex !== -1)) {
360+
// Multiple selection mode or single selection mode (select first repository)
361+
this._repositories.forEach((repositoryView, index) => {
362+
if (repositoryView.selectionIndex === -1) {
363+
added.push(repositoryView.repository);
364+
}
365+
repositoryView.selectionIndex = index;
366+
});
367+
368+
this._onDidChangeRepositories.fire({ added, removed: Iterable.empty() });
369+
}
370+
330371
this.didSelectRepository = false;
331372
return;
332373
}
@@ -352,10 +393,18 @@ export class SCMViewService implements ISCMViewService {
352393
}
353394
}
354395

355-
const maxSelectionIndex = this.getMaxSelectionIndex();
356-
this.insertRepositoryView(this._repositories, { ...repositoryView, selectionIndex: maxSelectionIndex + 1 });
357-
this._onDidChangeRepositories.fire({ added: [repositoryView.repository], removed });
396+
if (this._selectionModeConfig.get() === 'multiple' || !this._repositories.find(r => r.selectionIndex !== -1)) {
397+
// Multiple selection mode or single selection mode (select first repository)
398+
const maxSelectionIndex = this.getMaxSelectionIndex();
399+
this.insertRepositoryView(this._repositories, { ...repositoryView, selectionIndex: maxSelectionIndex + 1 });
400+
this._onDidChangeRepositories.fire({ added: [repositoryView.repository], removed });
401+
} else {
402+
// Single selection mode (add subsequent repository)
403+
this.insertRepositoryView(this._repositories, repositoryView);
404+
this._onDidChangeRepositories.fire({ added: Iterable.empty(), removed });
405+
}
358406

407+
// Focus repository if nothing is focused
359408
if (!this._repositories.find(r => r.focused)) {
360409
this.focus(repository);
361410
}
@@ -410,7 +459,11 @@ export class SCMViewService implements ISCMViewService {
410459
}
411460

412461
if (visible) {
413-
this.visibleRepositories = [...this.visibleRepositories, repository];
462+
if (this._selectionModeConfig.get() === 'single') {
463+
this.visibleRepositories = [repository];
464+
} else if (this._selectionModeConfig.get() === 'multiple') {
465+
this.visibleRepositories = [...this.visibleRepositories, repository];
466+
}
414467
} else {
415468
const index = this.visibleRepositories.indexOf(repository);
416469

@@ -445,6 +498,7 @@ export class SCMViewService implements ISCMViewService {
445498

446499
pinActiveRepository(repository: ISCMRepository | undefined): void {
447500
this._activeRepositoryPinnedObs.set(repository, undefined);
501+
this._repositoryPinnedContextKey.set(!!repository);
448502
}
449503

450504
private compareRepositories(op1: ISCMRepositoryView, op2: ISCMRepositoryView): number {

0 commit comments

Comments
 (0)