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

Benibenj/registerContextKeyHandler #213135

Merged
merged 29 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
106aecc
cleanup editor group context keys
benibenj May 17, 2024
57d18b9
Update src/vs/workbench/browser/parts/editor/editorPart.ts
benibenj May 17, 2024
61b4530
context key on parts
benibenj May 17, 2024
dcf2bda
Update global context keys
benibenj May 17, 2024
42ce9ac
remove scoped keys on group removal
benibenj May 18, 2024
1dd1b84
cleanup
bpasero May 19, 2024
be06d73
first draft contexkt key registration
benibenj May 20, 2024
0ba1c7d
Make it a provider
benibenj May 21, 2024
7cfa891
Use group instead of active editor
benibenj May 21, 2024
7656149
getGroupContextKeyValue
benibenj May 21, 2024
05561d4
Merge branch 'main' into benibenj/registerContextKeyHandler
benibenj May 21, 2024
e892938
doc
benibenj May 21, 2024
fdc46b8
Fix merge error
benibenj May 21, 2024
fdfd59e
:lipstick:
benibenj May 21, 2024
a6637a8
Merge branch 'main' into benibenj/registerContextKeyHandler
bpasero May 22, 2024
d6631ab
Do not run provers when active editor changed
benibenj May 22, 2024
0f73558
:lipstick:
benibenj May 22, 2024
660c3bf
generics
benibenj May 22, 2024
aa05b33
:lipstick:
benibenj May 22, 2024
99d8a9f
:lipstick:
benibenj May 22, 2024
df3519d
:lipstick:
benibenj May 22, 2024
df2e509
Update src/vs/workbench/browser/parts/editor/editorParts.ts
benibenj May 23, 2024
652b533
:lipstick:
benibenj May 23, 2024
4d8c7d7
Add tests
benibenj May 23, 2024
e9cd920
shuffle a bit
bpasero May 23, 2024
5936c89
:lipstick:
benibenj May 23, 2024
5f1a491
:lipstick:
benibenj May 23, 2024
f9719d5
:lipstick:
bpasero May 23, 2024
37ded83
Add test that finds bug and fix the bug
benibenj May 23, 2024
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
81 changes: 75 additions & 6 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 @@ -127,9 +127,13 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor
this.updateGlobalContextKeys();
this._onDidActiveGroupChange.fire(group);
}));
disposables.add(part.onDidAddGroup(group => this._onDidAddGroup.fire(group)));
disposables.add(part.onDidAddGroup(group => {
this.registerRunGroupContextKeyProviders(group);
this._onDidAddGroup.fire(group);
}));
disposables.add(part.onDidRemoveGroup(group => {
this.removeGroupScopedContextKeys(group);
this.unregisterRunGroupContextKeyProviders(group);
this._onDidRemoveGroup.fire(group);
}));
disposables.add(part.onDidMoveGroup(group => this._onDidMoveGroup.fire(group)));
benibenj marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -696,10 +700,75 @@ export class EditorParts extends MultiWindowParts<EditorPart> implements IEditor
}

private removeGroupScopedContextKeys(group: IEditorGroupView): void {
const groupScopedContextKeys = this.scopedContextKeys.get(group.id);
if (groupScopedContextKeys) {
this.scopedContextKeys.delete(group.id);
this.scopedContextKeys.delete(group.id);
this.registeredContextKeys.delete(group.id);
}

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.`);
}

this.contextKeyProviders.set(provider.contextKey.key, provider);

benibenj marked this conversation as resolved.
Show resolved Hide resolved
const setContextKeyForGroups = () => {
for (const group of this.groups) {
this.runRegisteredContextKeyProvider(group, provider);
}
};

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

return toDisposable(() => {
onDidChange?.dispose();
this.contextKeyProviders.delete(provider.contextKey.key);
this.globalContextKeys.delete(provider.contextKey.key);
this.scopedContextKeys.forEach(scopedContextKeys => scopedContextKeys.delete(provider.contextKey.key));
this.registeredContextKeys.forEach(registeredContextKeys => registeredContextKeys.delete(provider.contextKey.key));
});
}

private groupContextKeyProvidersRunners = this._register(new DisposableMap<GroupIdentifier, IDisposable>());
benibenj marked this conversation as resolved.
Show resolved Hide resolved
private registerRunGroupContextKeyProviders(group: IEditorGroupView): void {
bpasero marked this conversation as resolved.
Show resolved Hide resolved
if (this.groupContextKeyProvidersRunners.has(group.id)) {
throw new Error(`A context key provider runner for group ${group.id} already exists.`);
}
benibenj marked this conversation as resolved.
Show resolved Hide resolved

// Run the context key providers when the active editor changes
const disposable = group.onDidActiveEditorChange(() => {
for (const contextKeyProvider of this.contextKeyProviders.values()) {
bpasero marked this conversation as resolved.
Show resolved Hide resolved
this.runRegisteredContextKeyProvider(group, contextKeyProvider);
}
});
this.groupContextKeyProvidersRunners.set(group.id, disposable);
}

private unregisterRunGroupContextKeyProviders(group: IEditorGroupView): void {
this.groupContextKeyProvidersRunners.deleteAndDispose(group.id);
}

private runRegisteredContextKeyProvider<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();
}
}
27 changes: 26 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,23 @@ 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 +578,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
7 changes: 5 additions & 2 deletions src/vs/workbench/test/browser/workbenchTestServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService
import { ITextResourceConfigurationService, ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration';
import { IPosition, Position as EditorPosition } from 'vs/editor/common/core/position';
import { IMenuService, MenuId, IMenu, IMenuChangeEvent } from 'vs/platform/actions/common/actions';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ContextKeyValue, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { MockContextKeyService, MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { ITextBufferFactory, DefaultEndOfLine, EndOfLinePreference, ITextSnapshot } from 'vs/editor/common/model';
import { Range } from 'vs/editor/common/core/range';
Expand All @@ -52,7 +52,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/common/decorations';
import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IMergeGroupOptions, IEditorReplacement, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorPart, IAuxiliaryEditorPart, IEditorGroupsContainer, IAuxiliaryEditorPartCreateEvent, IEditorWorkingSet } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IMergeGroupOptions, IEditorReplacement, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions, GroupOrientation, ICloseAllEditorsOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorPart, IAuxiliaryEditorPart, IEditorGroupsContainer, IAuxiliaryEditorPartCreateEvent, IEditorWorkingSet, IEditorGroupContextKeyProvider } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorService, ISaveEditorsOptions, IRevertAllEditorsOptions, PreferredGroup, IEditorsChangeEvent, ISaveEditorsResult } from 'vs/workbench/services/editor/common/editorService';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { IEditorPaneRegistry, EditorPaneDescriptor } from 'vs/workbench/browser/editor';
Expand Down Expand Up @@ -867,6 +867,7 @@ export class TestEditorGroupsService implements IEditorGroupsService {
centerLayout(active: boolean): void { }
isLayoutCentered(): boolean { return false; }
createEditorDropTarget(container: HTMLElement, delegate: IEditorDropTargetDelegate): IDisposable { return Disposable.None; }
registerContextKeyProvider<T extends ContextKeyValue>(_provider: IEditorGroupContextKeyProvider<T>): IDisposable { throw new Error('not implemented'); }

partOptions!: IEditorPartOptions;
enforcePartOptions(options: IEditorPartOptions): IDisposable { return Disposable.None; }
Expand Down Expand Up @@ -1842,6 +1843,8 @@ export class TestEditorPart extends MainEditorPart implements IEditorGroupsServi
getWorkingSets(): IEditorWorkingSet[] { throw new Error('Method not implemented.'); }
applyWorkingSet(workingSet: IEditorWorkingSet | 'empty'): Promise<boolean> { throw new Error('Method not implemented.'); }
deleteWorkingSet(workingSet: IEditorWorkingSet): Promise<boolean> { throw new Error('Method not implemented.'); }

registerContextKeyProvider<T extends ContextKeyValue>(provider: IEditorGroupContextKeyProvider<T>): IDisposable { throw new Error('Method not implemented.'); }
}

export async function createEditorPart(instantiationService: IInstantiationService, disposables: DisposableStore): Promise<TestEditorPart> {
Expand Down
Loading