Benibenj/registerContextKeyHandler (#213135)

* 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>
This commit is contained in:
Benjamin Christopher Simmonds 2024-05-23 17:52:39 +02:00 committed by GitHub
parent 343a048566
commit ca45e3d78e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 365 additions and 68 deletions

View file

@ -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';
@ -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,
@ -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 {
@ -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)));
@ -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;
@ -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
@ -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

View file

@ -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';
@ -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') {
@ -291,19 +293,17 @@ 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);
@ -311,26 +311,42 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu
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(
@ -338,27 +354,37 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu
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();
}
}

View file

@ -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';
@ -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.
*/
@ -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 {

View file

@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, createEditorPart, ITestInstantiationService, workbenchTeardown } from 'vs/workbench/test/browser/workbenchTestServices';
import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService';
import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, TestServiceAccessor, ITestInstantiationService, workbenchTeardown, createEditorParts, TestEditorParts } from 'vs/workbench/test/browser/workbenchTestServices';
import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupLocation, isEditorGroup, IEditorGroupsService, GroupsArrangement, IEditorGroupContextKeyProvider } from 'vs/workbench/services/editor/common/editorGroupsService';
import { CloseDirection, IEditorPartOptions, EditorsOrder, EditorInputCapabilities, GroupModelChangeKind, SideBySideEditor, IEditorFactoryRegistry, EditorExtensions } from 'vs/workbench/common/editor';
import { URI } from 'vs/base/common/uri';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
@ -19,6 +19,9 @@ import { IGroupModelChangeEvent, IGroupEditorMoveEvent, IGroupEditorOpenEvent }
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { Registry } from 'vs/platform/registry/common/platform';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { Emitter } from 'vs/base/common/event';
import { isEqual } from 'vs/base/common/resources';
suite('EditorGroupsService', () => {
@ -42,14 +45,19 @@ suite('EditorGroupsService', () => {
disposables.clear();
});
async function createPart(instantiationService = workbenchInstantiationService(undefined, disposables)): Promise<[TestEditorPart, TestInstantiationService]> {
async function createParts(instantiationService = workbenchInstantiationService(undefined, disposables)): Promise<[TestEditorParts, TestInstantiationService]> {
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const part = await createEditorPart(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, part);
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
testLocalInstantiationService = instantiationService;
return [part, instantiationService];
return [parts, instantiationService];
}
async function createPart(instantiationService?: TestInstantiationService): Promise<[TestEditorPart, TestInstantiationService]> {
const [parts, testInstantiationService] = await createParts(instantiationService);
return [parts.testMainPart, testInstantiationService];
}
function createTestFileEditorInput(resource: URI, typeId: string): TestFileEditorInput {
@ -2027,5 +2035,167 @@ suite('EditorGroupsService', () => {
assert.strictEqual(part.activeGroup.isEmpty, true);
});
test('context key provider', async function () {
const disposables = new DisposableStore();
// Instantiate workbench and setup initial state
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
const rootContextKeyService = instantiationService.get(IContextKeyService);
const [parts] = await createParts(instantiationService);
const input1 = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID);
const input3 = createTestFileEditorInput(URI.file('foo/bar3'), TEST_EDITOR_INPUT_ID);
const group1 = parts.activeGroup;
const group2 = parts.addGroup(group1, GroupDirection.RIGHT);
await group2.openEditor(input2, { pinned: true });
await group1.openEditor(input1, { pinned: true });
// Create context key provider
const rawContextKey = new RawContextKey<number>('testContextKey', parts.activeGroup.id);
const contextKeyProvider: IEditorGroupContextKeyProvider<number> = {
contextKey: rawContextKey,
getGroupContextKeyValue: (group) => group.id
};
disposables.add(parts.registerContextKeyProvider(contextKeyProvider));
// Initial state: group1 is active
assert.strictEqual(parts.activeGroup.id, group1.id);
let globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key);
let group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
let group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
assert.strictEqual(globalContextKeyValue, group1.id);
assert.strictEqual(group1ContextKeyValue, group1.id);
assert.strictEqual(group2ContextKeyValue, group2.id);
// Make group2 active and ensure both gloabal and local context key values are updated
parts.activateGroup(group2);
globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key);
group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
assert.strictEqual(globalContextKeyValue, group2.id);
assert.strictEqual(group1ContextKeyValue, group1.id);
assert.strictEqual(group2ContextKeyValue, group2.id);
// Add a new group and ensure both gloabal and local context key values are updated
// Group 3 will be active
const group3 = parts.addGroup(group2, GroupDirection.RIGHT);
await group3.openEditor(input3, { pinned: true });
globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key);
group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
const group3ContextKeyValue = group3.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
assert.strictEqual(globalContextKeyValue, group3.id);
assert.strictEqual(group1ContextKeyValue, group1.id);
assert.strictEqual(group2ContextKeyValue, group2.id);
assert.strictEqual(group3ContextKeyValue, group3.id);
disposables.dispose();
});
test('context key provider: onDidChange', async function () {
const disposables = new DisposableStore();
// Instantiate workbench and setup initial state
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
const rootContextKeyService = instantiationService.get(IContextKeyService);
const parts = await createEditorParts(instantiationService, disposables);
const input1 = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID);
const group1 = parts.activeGroup;
const group2 = parts.addGroup(group1, GroupDirection.RIGHT);
await group2.openEditor(input2, { pinned: true });
await group1.openEditor(input1, { pinned: true });
// Create context key provider
let offset = 0;
const _onDidChange = new Emitter<void>();
const rawContextKey = new RawContextKey<number>('testContextKey', parts.activeGroup.id);
const contextKeyProvider: IEditorGroupContextKeyProvider<number> = {
contextKey: rawContextKey,
getGroupContextKeyValue: (group) => group.id + offset,
onDidChange: _onDidChange.event
};
disposables.add(parts.registerContextKeyProvider(contextKeyProvider));
// Initial state: group1 is active
assert.strictEqual(parts.activeGroup.id, group1.id);
let globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key);
let group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
let group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
assert.strictEqual(globalContextKeyValue, group1.id + offset);
assert.strictEqual(group1ContextKeyValue, group1.id + offset);
assert.strictEqual(group2ContextKeyValue, group2.id + offset);
// Make a change to the context key provider and fire onDidChange such that all context key values are updated
offset = 10;
_onDidChange.fire();
globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key);
group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
group2ContextKeyValue = group2.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
assert.strictEqual(globalContextKeyValue, group1.id + offset);
assert.strictEqual(group1ContextKeyValue, group1.id + offset);
assert.strictEqual(group2ContextKeyValue, group2.id + offset);
disposables.dispose();
});
test('context key provider: active editor change', async function () {
const disposables = new DisposableStore();
// Instantiate workbench and setup initial state
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
const rootContextKeyService = instantiationService.get(IContextKeyService);
const parts = await createEditorParts(instantiationService, disposables);
const input1 = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
const input2 = createTestFileEditorInput(URI.file('foo/bar2'), TEST_EDITOR_INPUT_ID);
const group1 = parts.activeGroup;
await group1.openEditor(input2, { pinned: true });
await group1.openEditor(input1, { pinned: true });
// Create context key provider
const rawContextKey = new RawContextKey<string>('testContextKey', input1.resource.toString());
const contextKeyProvider: IEditorGroupContextKeyProvider<string> = {
contextKey: rawContextKey,
getGroupContextKeyValue: (group) => group.activeEditor?.resource?.toString() ?? '',
};
disposables.add(parts.registerContextKeyProvider(contextKeyProvider));
// Initial state: input1 is active
assert.strictEqual(isEqual(group1.activeEditor?.resource, input1.resource), true);
let globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key);
let group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
assert.strictEqual(globalContextKeyValue, input1.resource.toString());
assert.strictEqual(group1ContextKeyValue, input1.resource.toString());
// Make input2 active and ensure both gloabal and local context key values are updated
await group1.openEditor(input2);
globalContextKeyValue = rootContextKeyService.getContextKeyValue(rawContextKey.key);
group1ContextKeyValue = group1.scopedContextKeyService.getContextKeyValue(rawContextKey.key);
assert.strictEqual(globalContextKeyValue, input2.resource.toString());
assert.strictEqual(group1ContextKeyValue, input2.resource.toString());
disposables.dispose();
});
ensureNoDisposablesAreLeakedInTestSuite();
});

View file

@ -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';
@ -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';
@ -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; }
@ -1842,28 +1843,33 @@ 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> {
export class TestEditorParts extends EditorParts {
testMainPart!: TestEditorPart;
class TestEditorParts extends EditorParts {
protected override createMainEditorPart(): MainEditorPart {
this.testMainPart = this.instantiationService.createInstance(TestEditorPart, this);
testMainPart!: TestEditorPart;
protected override createMainEditorPart(): MainEditorPart {
this.testMainPart = instantiationService.createInstance(TestEditorPart, this);
return this.testMainPart;
}
return this.testMainPart;
}
}
const part = disposables.add(instantiationService.createInstance(TestEditorParts)).testMainPart;
export async function createEditorParts(instantiationService: IInstantiationService, disposables: DisposableStore): Promise<TestEditorParts> {
const parts = instantiationService.createInstance(TestEditorParts);
const part = disposables.add(parts).testMainPart;
part.create(document.createElement('div'));
part.layout(1080, 800, 0, 0);
await part.whenReady;
await parts.whenReady;
return part;
return parts;
}
export async function createEditorPart(instantiationService: IInstantiationService, disposables: DisposableStore): Promise<TestEditorPart> {
return (await createEditorParts(instantiationService, disposables)).testMainPart;
}
export class TestListService implements IListService {