Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
a27b3d4
fix: persist SCM repository visibility state across restarts
kno Mar 12, 2026
aa77e1b
Merge branch 'microsoft:main' into fix/scm-repo-visibility-persistence
kno Mar 15, 2026
bab0dd5
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 16, 2026
eea4838
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 16, 2026
bfc328e
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 18, 2026
c12df1b
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 18, 2026
808a84f
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 19, 2026
638520a
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 19, 2026
f92521f
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 19, 2026
002a7b9
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 20, 2026
b69c301
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 22, 2026
fe73559
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 23, 2026
f56e4a1
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 23, 2026
7a5a6e6
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 23, 2026
8ea803a
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 23, 2026
be25f9a
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 24, 2026
b973d30
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 24, 2026
2442c92
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 25, 2026
39a6b02
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 25, 2026
0028b6d
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 26, 2026
2b99a7e
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 26, 2026
a2f96f1
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 26, 2026
56320a7
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 26, 2026
c755433
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 27, 2026
40dc688
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 27, 2026
7232bdd
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 27, 2026
bd87813
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 27, 2026
77ed85e
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 27, 2026
e4a7e9d
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 27, 2026
a9abe1e
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 30, 2026
da2b39e
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 30, 2026
3d3b5a2
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 31, 2026
79e9c73
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 31, 2026
22ce550
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Mar 31, 2026
6bdbf76
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 1, 2026
8c06911
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 1, 2026
df9f39f
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 6, 2026
21679a9
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 7, 2026
dc31e7a
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 7, 2026
b6c38b5
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 8, 2026
e8b03f6
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 8, 2026
a6dde4f
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 9, 2026
c341af8
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 9, 2026
1809d67
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 10, 2026
45490e9
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 12, 2026
b8f4854
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 13, 2026
3959186
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 16, 2026
9154e13
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 17, 2026
937ef70
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 17, 2026
f3da4b7
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 20, 2026
8f50f98
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 20, 2026
7cc6ad9
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 21, 2026
85f305f
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 22, 2026
9365715
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 25, 2026
4862a46
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 26, 2026
7838d50
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 27, 2026
367e913
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 28, 2026
31959e5
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Apr 30, 2026
eb18197
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno May 2, 2026
e21e7eb
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno May 4, 2026
6fd8d73
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno May 4, 2026
445313c
fix: address PR review comments
kno May 4, 2026
85a17da
feat: keep repository visibility context menu open
kno May 4, 2026
1408e65
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno May 5, 2026
c73dae2
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno May 6, 2026
fcad489
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno May 8, 2026
bfa612a
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno May 9, 2026
e0de879
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno May 11, 2026
87c9959
fix(scm): correct removed event and single-select restoration when ne…
aruizdesamaniego-sh May 12, 2026
0024ccf
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno May 13, 2026
65c64f4
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno May 14, 2026
c19ee2c
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno May 18, 2026
e520d03
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno May 19, 2026
c658267
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 3, 2026
82c9c42
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 5, 2026
a4aafae
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 6, 2026
897b9d0
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 6, 2026
32e3654
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 8, 2026
885591e
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 8, 2026
600bf58
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 10, 2026
5716cf7
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 15, 2026
3ce205c
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 15, 2026
a6de015
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 15, 2026
af6ddfa
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 18, 2026
accad95
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 18, 2026
2f1807b
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 22, 2026
ef53ce5
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 24, 2026
7954fb7
Merge branch 'main' into fix/scm-repo-visibility-persistence
kno Jun 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/vs/base/browser/ui/menu/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { DisposableStore } from '../../../common/lifecycle.js';
import { isLinux, isMacintosh } from '../../../common/platform.js';
import { ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js';
import * as strings from '../../../common/strings.js';
import { hasKey } from '../../../common/types.js';
import { AnchorAlignment, layout, LayoutAnchorPosition } from '../../../common/layout.js';

export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/;
Expand Down Expand Up @@ -237,6 +238,22 @@ export class Menu extends ActionBar {
parent: this
};

// When a keepOpen action runs, refresh the checked/enabled state of all items
this._register(this.onDidRun(e => {
if (e.action && hasKey(e.action, 'keepOpen') && e.action.keepOpen) {
for (const item of this.viewItems) {
if (item instanceof BaseMenuActionViewItem) {
// Re-evaluate the action's state from context keys
if (hasKey(item.action, 'refreshState')) {
Comment on lines +243 to +247
(item.action as { refreshState(): void }).refreshState();
}
// Update the visual state
item.refreshState();
}
}
}
}));

this.mnemonics = new Map<string, Array<BaseMenuActionViewItem>>();

// Scroll Logic
Expand Down Expand Up @@ -700,6 +717,15 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
return this.mnemonic;
}

/**
* Refreshes the checked and enabled visual state of this menu item.
* Used when a keepOpen action runs and the menu stays visible.
*/
refreshState(): void {
this.updateChecked();
this.updateEnabled();
}

protected applyStyle(): void {
const isSelected = this.element && this.element.classList.contains('focused');
const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor;
Expand Down
32 changes: 31 additions & 1 deletion src/vs/platform/actions/common/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export interface IMenuItem {
group?: 'navigation' | string;
order?: number;
isHiddenByDefault?: boolean;
/**
* When true, the context menu will not close when this item is triggered.
* This is useful for toggle actions in menus where the user may want to
* change multiple items without having to reopen the menu.
*/
keepOpen?: boolean;
Comment on lines +28 to +33
}

export interface ISubmenuItem {
Expand Down Expand Up @@ -583,6 +589,9 @@ export class MenuItemAction implements IAction {
readonly alt: MenuItemAction | undefined;

private readonly _options: IMenuActionOptions | undefined;
private readonly _contextKeyService: IContextKeyService;
private readonly _precondition: ContextKeyExpression | undefined;
private readonly _toggledCondition: ContextKeyExpression | undefined;

readonly id: string;
readonly label: string;
Expand All @@ -591,15 +600,23 @@ export class MenuItemAction implements IAction {
readonly enabled: boolean;
readonly checked?: boolean;

/**
* When true, the context menu should not close when this item is triggered.
*/
keepOpen: boolean = false;

constructor(
item: ICommandAction,
alt: ICommandAction | undefined,
options: IMenuActionOptions | undefined,
readonly hideActions: IMenuItemHide | undefined,
readonly menuKeybinding: IAction | undefined,
@IContextKeyService contextKeyService: IContextKeyService,
@ICommandService private _commandService: ICommandService
@ICommandService private _commandService: ICommandService,
) {
this._contextKeyService = contextKeyService;
this._precondition = item.precondition;

this.id = item.id;
this.label = MenuItemAction.label(item, options);
this.tooltip = (typeof item.tooltip === 'string' ? item.tooltip : item.tooltip?.value) ?? '';
Expand All @@ -608,10 +625,12 @@ export class MenuItemAction implements IAction {

let icon: ThemeIcon | undefined;

this._toggledCondition = undefined;
if (item.toggled) {
const toggled = ((item.toggled as { condition: ContextKeyExpression }).condition ? item.toggled : { condition: item.toggled }) as {
condition: ContextKeyExpression; icon?: Icon; tooltip?: string | ILocalizedString; title?: string | ILocalizedString;
};
this._toggledCondition = toggled.condition;
this.checked = contextKeyService.contextMatchesRules(toggled.condition);
if (this.checked && toggled.tooltip) {
this.tooltip = typeof toggled.tooltip === 'string' ? toggled.tooltip : toggled.tooltip.value;
Expand All @@ -637,6 +656,17 @@ export class MenuItemAction implements IAction {

}

/**
* Re-evaluates the `checked` and `enabled` state from the current context keys.
* Used when the menu stays open after this action runs (keepOpen).
*/
refreshState(): void {
(this as unknown as { enabled: boolean }).enabled = !this._precondition || this._contextKeyService.contextMatchesRules(this._precondition);
(this as unknown as { checked: boolean | undefined }).checked = this._toggledCondition
? this._contextKeyService.contextMatchesRules(this._toggledCondition)
: undefined;
}
Comment on lines +663 to +668

run(...args: unknown[]): Promise<void> {
let runArgs: unknown[] = [];

Expand Down
4 changes: 3 additions & 1 deletion src/vs/platform/actions/common/menuService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,9 @@ class MenuInfo extends MenuInfoSnapshot {
if (isMenuItem) {
// MenuItemAction
const menuKeybinding = createConfigureKeybindingAction(this._commandService, this._keybindingService, item.command.id, item.when);
(activeActions ??= []).push(new MenuItemAction(item.command, item.alt, options, menuHide, menuKeybinding, this._contextKeyService, this._commandService));
const menuItemAction = new MenuItemAction(item.command, item.alt, options, menuHide, menuKeybinding, this._contextKeyService, this._commandService);
menuItemAction.keepOpen = !!item.keepOpen;
(activeActions ??= []).push(menuItemAction);
} else {
// SubmenuItemAction
const groups = new MenuInfo(item.submenu, this._hiddenStates, this._collectContextKeysForSubmenus, this._commandService, this._keybindingService, this._contextKeyService).createActionGroups(options);
Expand Down
5 changes: 5 additions & 0 deletions src/vs/platform/contextview/browser/contextMenuHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { IKeybindingService } from '../../keybinding/common/keybinding.js';
import { INotificationService } from '../../notification/common/notification.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { defaultMenuStyles } from '../../theme/browser/defaultStyles.js';
import { MenuItemAction } from '../../actions/common/actions.js';


export interface IContextMenuHandlerOptions {
Expand Down Expand Up @@ -153,6 +154,10 @@ export class ContextMenuHandler {
this.telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: e.action.id, from: 'contextMenu' });
}

if (e.action instanceof MenuItemAction && e.action.keepOpen) {
return;
}

this.contextViewService.hideContextView(false);
}

Expand Down
5 changes: 5 additions & 0 deletions src/vs/workbench/contrib/scm/browser/menus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,11 @@ export class SCMMenus implements ISCMMenus, IDisposable {
}

dispose(): void {
this.titleMenu.dispose();
for (const [, item] of this.menus) {
item.dispose();
}
this.menus.clear();
this.disposables.dispose();
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/contrib/scm/browser/scmViewPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,7 @@ class RepositoryVisibilityAction extends Action2 {
f1: false,
precondition: ContextKeyExpr.or(ContextKeys.RepositoryVisibilityCount.notEqualsTo(1), ContextKeys.RepositoryVisibility(repository).isEqualTo(false)),
toggled: ContextKeys.RepositoryVisibility(repository).isEqualTo(true),
menu: { id: Menus.Repositories, group: '0_repositories' }
menu: { id: Menus.Repositories, group: '0_repositories', keepOpen: true }
});
this.repository = repository;
}
Expand Down
62 changes: 41 additions & 21 deletions src/vs/workbench/contrib/scm/browser/scmViewService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export class SCMViewService implements ISCMViewService {
@IStorageService private readonly storageService: IStorageService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService
) {
this.menus = instantiationService.createInstance(SCMMenus);
this.menus = this.disposables.add(instantiationService.createInstance(SCMMenus));

const explorerEnabledConfig = observableConfigValue<boolean>('scm.repositories.explorer', false, this.configurationService);
this.graphShowIncomingChangesConfig = observableConfigValue<boolean>('scm.graph.showIncomingChanges', true, this.configurationService);
Expand Down Expand Up @@ -357,32 +357,34 @@ export class SCMViewService implements ISCMViewService {
} satisfies ISCMRepositoryView;

let removed: Iterable<ISCMRepository> = Iterable.empty();
let newReposToReAdd: ISCMRepositoryView[] = [];

if (this.previousState && !this.didFinishLoadingRepositories.get()) {
// Hidden repositories are not part of the saved state, so skip
// the restoration logic for them. They are still added to the
// internal list but should not affect the visibility restoration.
if (repository.provider.isHidden) {
this.insertRepositoryView(this._repositories, repositoryView);
this._onDidChangeRepositories.fire({ added: Iterable.empty(), removed: Iterable.empty() });
return;
}

const index = this.previousState.all.indexOf(getProviderStorageKey(repository.provider));

if (index === -1) {
// This repository is not part of the previous state which means that it
// was either manually closed in the previous session, or the repository
// was added after the previous session. In this case, we should select
// all of the repositories.
const added: ISCMRepository[] = [];

this.insertRepositoryView(this._repositories, repositoryView);

// This repository is not part of the previous state which means
// it was added after the previous session. In multi-select mode
// (or if no repository is selected yet), add it as visible. In
// single-select mode with a selection, add it but not visible.
if (this.selectionModeConfig.get() === ISCMRepositorySelectionMode.Multiple || !this._repositories.find(r => r.selectionIndex !== -1)) {
// Multiple selection mode or single selection mode (select first repository)
this._repositories.forEach((repositoryView, index) => {
if (repositoryView.selectionIndex === -1) {
added.push(repositoryView.repository);
}
repositoryView.selectionIndex = index;
});

this._onDidChangeRepositories.fire({ added, removed: Iterable.empty() });
const maxSelectionIndex = this.getMaxSelectionIndex();
this.insertRepositoryView(this._repositories, { ...repositoryView, selectionIndex: maxSelectionIndex + 1 });
this._onDidChangeRepositories.fire({ added: [repositoryView.repository], removed: Iterable.empty() });
Comment thread
kno marked this conversation as resolved.
} else {
this.insertRepositoryView(this._repositories, repositoryView);
this._onDidChangeRepositories.fire({ added: Iterable.empty(), removed: Iterable.empty() });
Comment thread
kno marked this conversation as resolved.
}

this.didSelectRepository = false;
return;
}

Expand All @@ -396,7 +398,15 @@ export class SCMViewService implements ISCMViewService {
} else {
// First visible repository
if (!this.didSelectRepository) {
removed = [...this.visibleRepositories];
newReposToReAdd = this._repositories.filter(r =>
r.selectionIndex !== -1 &&
this.previousState!.all.indexOf(getProviderStorageKey(r.repository.provider)) === -1
);

removed = [...this.visibleRepositories].filter(r =>
this.previousState!.all.indexOf(getProviderStorageKey(r.provider)) !== -1
);

this._repositories.forEach(r => {
r.focused = false;
r.selectionIndex = -1;
Expand All @@ -411,7 +421,17 @@ export class SCMViewService implements ISCMViewService {
// Multiple selection mode or single selection mode (select first repository)
const maxSelectionIndex = this.getMaxSelectionIndex();
this.insertRepositoryView(this._repositories, { ...repositoryView, selectionIndex: maxSelectionIndex + 1 });
this._onDidChangeRepositories.fire({ added: [repositoryView.repository], removed });

if (newReposToReAdd.length > 0 && this.selectionModeConfig.get() === ISCMRepositorySelectionMode.Multiple) {
const addedRepos: ISCMRepository[] = [repositoryView.repository];
for (const r of newReposToReAdd) {
r.selectionIndex = this.getMaxSelectionIndex() + 1;
addedRepos.push(r.repository);
}
this._onDidChangeRepositories.fire({ added: addedRepos, removed });
} else {
this._onDidChangeRepositories.fire({ added: [repositoryView.repository], removed });
}
} else {
// Single selection mode (add subsequent repository)
this.insertRepositoryView(this._repositories, repositoryView);
Expand Down
6 changes: 6 additions & 0 deletions src/vs/workbench/contrib/scm/common/scmService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,12 @@ export class SCMService implements ISCMService {
return repository;
}

dispose(): void {
this.inputHistory.dispose();
this._onDidAddProvider.dispose();
this._onDidRemoveProvider.dispose();
}

getRepository(id: string): ISCMRepository | undefined;
getRepository(resource: URI): ISCMRepository | undefined;
getRepository(idOrResource: string | URI): ISCMRepository | undefined {
Expand Down
Loading