Skip to content

Commit

Permalink
Benibenj/registerContextKeyHandler (microsoft#213135)
Browse files Browse the repository at this point in the history
* cleanup editor group context keys

* Update src/vs/workbench/browser/parts/editor/editorPart.ts

Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>

* context key on parts

* Update global context keys

* remove scoped keys on group removal

* cleanup

* first draft contexkt key registration

* Make it a provider

* Use group instead of active editor

* getGroupContextKeyValue

* doc

* Fix merge error

* 💄

---------

Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
  • Loading branch information
2 people authored and andremmsilva committed May 26, 2024
1 parent dbcdb4e commit 37ad7e2
Show file tree
Hide file tree
Showing 5 changed files with 365 additions and 68 deletions.
123 changes: 96 additions & 27 deletions src/vs/workbench/browser/parts/editor/editorParts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------------*/

import { localize } from 'vs/nls';
import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IAuxiliaryEditorPartCreateEvent, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService';
import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IAuxiliaryEditorPartCreateEvent, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions } from 'vs/workbench/services/editor/common/editorGroupsService';
import { Emitter } from 'vs/base/common/event';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { GroupIdentifier } from 'vs/workbench/common/editor';
import { EditorPart, IEditorPartUIState, MainEditorPart } from 'vs/workbench/browser/parts/editor/editorPart';
import { IEditorGroupView, IEditorPartsView } from 'vs/workbench/browser/parts/editor/editor';
Expand Down Expand Up @@ -46,7 +46,7 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor
private mostRecentActiveParts = [this.mainPart];

constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@IStorageService private readonly storageService: IStorageService,
@IThemeService themeService: IThemeService,
@IAuxiliaryWindowService private readonly auxiliaryWindowService: IAuxiliaryWindowService,
Expand All @@ -62,6 +62,7 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor

private registerListeners(): void {
this._register(this.onDidChangeMementoValue(StorageScope.WORKSPACE, this._store)(e => this.onDidChangeMementoState(e)));
this.whenReady.then(() => this.registerGroupsContextKeyListeners());
}

protected createMainEditorPart(): MainEditorPart {
Expand Down Expand Up @@ -123,15 +124,9 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor
}));
disposables.add(toDisposable(() => this.doUpdateMostRecentActive(part)));

disposables.add(part.onDidChangeActiveGroup(group => {
this.updateGlobalContextKeys();
this._onDidActiveGroupChange.fire(group);
}));
disposables.add(part.onDidChangeActiveGroup(group => this._onDidActiveGroupChange.fire(group)));
disposables.add(part.onDidAddGroup(group => this._onDidAddGroup.fire(group)));
disposables.add(part.onDidRemoveGroup(group => {
this.removeGroupScopedContextKeys(group);
this._onDidRemoveGroup.fire(group);
}));
disposables.add(part.onDidRemoveGroup(group => this._onDidRemoveGroup.fire(group)));
disposables.add(part.onDidMoveGroup(group => this._onDidMoveGroup.fire(group)));
disposables.add(part.onDidActivateGroup(group => this._onDidActivateGroup.fire(group)));
disposables.add(part.onDidChangeGroupMaximized(maximized => this._onDidChangeGroupMaximized.fire(maximized)));
Expand Down Expand Up @@ -456,7 +451,7 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor

//#endregion

//#region Editor Groups Service
//#region Group Management

get activeGroup(): IEditorGroupView {
return this.activePart.activeGroup;
Expand Down Expand Up @@ -635,9 +630,40 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor
return this.getPart(container).createEditorDropTarget(container, delegate);
}

//#endregion

//#region Editor Group Context Key Handling

private readonly globalContextKeys = new Map<string, IContextKey<ContextKeyValue>>();
private readonly scopedContextKeys = new Map<GroupIdentifier, Map<string, IContextKey<ContextKeyValue>>>();

private registerGroupsContextKeyListeners(): void {
this._register(this.onDidChangeActiveGroup(() => this.updateGlobalContextKeys()));
this.groups.forEach(group => this.registerGroupContextKeyProvidersListeners(group));
this._register(this.onDidAddGroup(group => this.registerGroupContextKeyProvidersListeners(group)));
this._register(this.onDidRemoveGroup(group => {
this.scopedContextKeys.delete(group.id);
this.registeredContextKeys.delete(group.id);
this.contextKeyProviderDisposables.deleteAndDispose(group.id);
}));
}

private updateGlobalContextKeys(): void {
const activeGroupScopedContextKeys = this.scopedContextKeys.get(this.activeGroup.id);
if (!activeGroupScopedContextKeys) {
return;
}

for (const [key, globalContextKey] of this.globalContextKeys) {
const scopedContextKey = activeGroupScopedContextKeys.get(key);
if (scopedContextKey) {
globalContextKey.set(scopedContextKey.get());
} else {
globalContextKey.reset();
}
}
}

bind<T extends ContextKeyValue>(contextKey: RawContextKey<T>, group: IEditorGroupView): IContextKey<T> {

// Ensure we only bind to the same context key once globaly
Expand Down Expand Up @@ -679,27 +705,70 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor
};
}

private updateGlobalContextKeys(): void {
const activeGroupScopedContextKeys = this.scopedContextKeys.get(this.activeGroup.id);
if (!activeGroupScopedContextKeys) {
return;
private readonly contextKeyProviders = new Map<string, IEditorGroupContextKeyProvider<ContextKeyValue>>();
private readonly registeredContextKeys = new Map<GroupIdentifier, Map<string, IContextKey>>();

registerContextKeyProvider<T extends ContextKeyValue>(provider: IEditorGroupContextKeyProvider<T>): IDisposable {
if (this.contextKeyProviders.has(provider.contextKey.key) || this.globalContextKeys.has(provider.contextKey.key)) {
throw new Error(`A context key provider for key ${provider.contextKey.key} already exists.`);
}

for (const [key, globalContextKey] of this.globalContextKeys) {
const scopedContextKey = activeGroupScopedContextKeys.get(key);
if (scopedContextKey) {
globalContextKey.set(scopedContextKey.get());
} else {
globalContextKey.reset();
this.contextKeyProviders.set(provider.contextKey.key, provider);

const setContextKeyForGroups = () => {
for (const group of this.groups) {
this.updateRegisteredContextKey(group, provider);
}
}
};

// Run initially and on change
setContextKeyForGroups();
const onDidChange = provider.onDidChange?.(() => setContextKeyForGroups());

return toDisposable(() => {
onDidChange?.dispose();

this.globalContextKeys.delete(provider.contextKey.key);
this.scopedContextKeys.forEach(scopedContextKeys => scopedContextKeys.delete(provider.contextKey.key));

this.contextKeyProviders.delete(provider.contextKey.key);
this.registeredContextKeys.forEach(registeredContextKeys => registeredContextKeys.delete(provider.contextKey.key));
});
}

private removeGroupScopedContextKeys(group: IEditorGroupView): void {
const groupScopedContextKeys = this.scopedContextKeys.get(group.id);
if (groupScopedContextKeys) {
this.scopedContextKeys.delete(group.id);
private readonly contextKeyProviderDisposables = this._register(new DisposableMap<GroupIdentifier, IDisposable>());
private registerGroupContextKeyProvidersListeners(group: IEditorGroupView): void {

// Update context keys from providers for the group when its active editor changes
const disposable = group.onDidActiveEditorChange(() => {
for (const contextKeyProvider of this.contextKeyProviders.values()) {
this.updateRegisteredContextKey(group, contextKeyProvider);
}
});

this.contextKeyProviderDisposables.set(group.id, disposable);
}

private updateRegisteredContextKey<T extends ContextKeyValue>(group: IEditorGroupView, provider: IEditorGroupContextKeyProvider<T>): void {

// Get the group scoped context keys for the provider
// If the providers context key has not yet been bound
// to the group, do so now.

let groupRegisteredContextKeys = this.registeredContextKeys.get(group.id);
if (!groupRegisteredContextKeys) {
groupRegisteredContextKeys = new Map<string, IContextKey>();
this.scopedContextKeys.set(group.id, groupRegisteredContextKeys);
}

let scopedRegisteredContextKey = groupRegisteredContextKeys.get(provider.contextKey.key);
if (!scopedRegisteredContextKey) {
scopedRegisteredContextKey = this.bind(provider.contextKey, group);
groupRegisteredContextKeys.set(provider.contextKey.key, scopedRegisteredContextKey);
}

// Set the context key value for the group context
scopedRegisteredContextKey.set(provider.getGroupContextKeyValue(group));
}

//#endregion
Expand Down
66 changes: 46 additions & 20 deletions src/vs/workbench/contrib/scm/browser/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { localize } from 'vs/nls';
import { basename } from 'vs/base/common/resources';
import { IDisposable, dispose, Disposable, DisposableStore, combinedDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { Emitter, Event } from 'vs/base/common/event';
import { VIEW_PANE_ID, ISCMService, ISCMRepository, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm';
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
Expand All @@ -19,6 +19,8 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'
import { Schemas } from 'vs/base/common/network';
import { Iterable } from 'vs/base/common/iterator';
import { ITitleService } from 'vs/workbench/services/title/browser/titleService';
import { IEditorGroupContextKeyProvider, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { EditorInput } from 'vs/workbench/common/editor/editorInput';

function getCount(repository: ISCMRepository): number {
if (typeof repository.provider.count === 'number') {
Expand Down Expand Up @@ -291,74 +293,98 @@ export class SCMActiveRepositoryContextKeyController implements IWorkbenchContri

export class SCMActiveResourceContextKeyController implements IWorkbenchContribution {

private activeResourceHasChangesContextKey: IContextKey<boolean>;
private activeResourceRepositoryContextKey: IContextKey<string | undefined>;
private readonly disposables = new DisposableStore();
private repositoryDisposables = new Set<IDisposable>();
private onDidRepositoryChange = new Emitter<void>();

constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IEditorService private readonly editorService: IEditorService,
@IEditorGroupsService editorGroupsService: IEditorGroupsService,
@ISCMService private readonly scmService: ISCMService,
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService
) {
this.activeResourceHasChangesContextKey = contextKeyService.createKey('scmActiveResourceHasChanges', false);
this.activeResourceRepositoryContextKey = contextKeyService.createKey('scmActiveResourceRepository', undefined);
const activeResourceHasChangesContextKey = new RawContextKey<boolean>('scmActiveResourceHasChanges', false, localize('scmActiveResourceHasChanges', "Whether the active resource has changes"));
const activeResourceRepositoryContextKey = new RawContextKey<string | undefined>('scmActiveResourceRepository', undefined, localize('scmActiveResourceRepository', "The active resource's repository"));

this.scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables);

for (const repository of this.scmService.repositories) {
this.onDidAddRepository(repository);
}

editorService.onDidActiveEditorChange(this.updateContextKey, this, this.disposables);
// Create context key providers which will update the context keys based on each groups active editor
const hasChangesContextKeyProvider: IEditorGroupContextKeyProvider<boolean> = {
contextKey: activeResourceHasChangesContextKey,
getGroupContextKeyValue: (group) => this.getEditorHasChanges(group.activeEditor),
onDidChange: this.onDidRepositoryChange.event
};

const repositoryContextKeyProvider: IEditorGroupContextKeyProvider<string | undefined> = {
contextKey: activeResourceRepositoryContextKey,
getGroupContextKeyValue: (group) => this.getEditorRepositoryId(group.activeEditor),
onDidChange: this.onDidRepositoryChange.event
};

this.disposables.add(editorGroupsService.registerContextKeyProvider(hasChangesContextKeyProvider));
this.disposables.add(editorGroupsService.registerContextKeyProvider(repositoryContextKeyProvider));
}

private onDidAddRepository(repository: ISCMRepository): void {
const onDidChange = Event.any(repository.provider.onDidChange, repository.provider.onDidChangeResources);
const changeDisposable = onDidChange(() => this.updateContextKey());
const changeDisposable = onDidChange(() => {
this.onDidRepositoryChange.fire();
});

const onDidRemove = Event.filter(this.scmService.onDidRemoveRepository, e => e === repository);
const removeDisposable = onDidRemove(() => {
disposable.dispose();
this.repositoryDisposables.delete(disposable);
this.updateContextKey();
this.onDidRepositoryChange.fire();
});

const disposable = combinedDisposable(changeDisposable, removeDisposable);
this.repositoryDisposables.add(disposable);
}

private updateContextKey(): void {
const activeResource = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor);
private getEditorRepositoryId(activeEditor: EditorInput | null): string | undefined {
const activeResource = EditorResourceAccessor.getOriginalUri(activeEditor);

if (activeResource?.scheme === Schemas.file || activeResource?.scheme === Schemas.vscodeRemote) {
const activeResourceRepository = Iterable.find(
this.scmService.repositories,
r => Boolean(r.provider.rootUri && this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri))
);

this.activeResourceRepositoryContextKey.set(activeResourceRepository?.id);
return activeResourceRepository?.id;
}

return undefined;
}

private getEditorHasChanges(activeEditor: EditorInput | null): boolean {
const activeResource = EditorResourceAccessor.getOriginalUri(activeEditor);

if (activeResource?.scheme === Schemas.file || activeResource?.scheme === Schemas.vscodeRemote) {
const activeResourceRepository = Iterable.find(
this.scmService.repositories,
r => Boolean(r.provider.rootUri && this.uriIdentityService.extUri.isEqualOrParent(activeResource, r.provider.rootUri))
);

for (const resourceGroup of activeResourceRepository?.provider.groups ?? []) {
if (resourceGroup.resources
.some(scmResource =>
this.uriIdentityService.extUri.isEqual(activeResource, scmResource.sourceUri))) {
this.activeResourceHasChangesContextKey.set(true);
return;
return true;
}
}

this.activeResourceHasChangesContextKey.set(false);
} else {
this.activeResourceHasChangesContextKey.set(false);
this.activeResourceRepositoryContextKey.set(undefined);
}

return false;
}

dispose(): void {
this.disposables.dispose();
dispose(this.repositoryDisposables.values());
this.repositoryDisposables.clear();
this.onDidRepositoryChange.dispose();
}
}
28 changes: 27 additions & 1 deletion src/vs/workbench/services/editor/common/editorGroupsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDimension } from 'vs/editor/common/core/dimension';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ContextKeyValue, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { URI } from 'vs/base/common/uri';
import { IGroupModelChangeEvent } from 'vs/workbench/common/editor/editorGroupModel';
import { IRectangle } from 'vs/platform/window/common/window';
Expand Down Expand Up @@ -491,6 +491,24 @@ export interface IEditorWorkingSet {
readonly name: string;
}

export interface IEditorGroupContextKeyProvider<T extends ContextKeyValue> {

/**
* The context key that needs to be set for each editor group context and the global context.
*/
readonly contextKey: RawContextKey<T>;

/**
* Retrieves the context key value for the given editor group.
*/
readonly getGroupContextKeyValue: (group: IEditorGroup) => T;

/**
* An event that is fired when there was a change leading to the context key value to be re-evaluated.
*/
readonly onDidChange?: Event<void>;
}

/**
* The main service to interact with editor groups across all opened editor parts.
*/
Expand Down Expand Up @@ -561,6 +579,14 @@ export interface IEditorGroupsService extends IEditorGroupsContainer {
* Deletes a working set.
*/
deleteWorkingSet(workingSet: IEditorWorkingSet): void;

/**
* Registers a context key provider. This provider sets a context key for each scoped editor group context and the global context.
*
* @param provider - The context key provider to be registered.
* @returns - A disposable object to unregister the provider.
*/
registerContextKeyProvider<T extends ContextKeyValue>(provider: IEditorGroupContextKeyProvider<T>): IDisposable;
}

export const enum OpenEditorContext {
Expand Down
Loading

0 comments on commit 37ad7e2

Please sign in to comment.