diff --git a/src/vs/workbench/browser/parts/editor/editorParts.ts b/src/vs/workbench/browser/parts/editor/editorParts.ts index 6b7a1907005e1..574c97e41539e 100644 --- a/src/vs/workbench/browser/parts/editor/editorParts.ts +++ b/src/vs/workbench/browser/parts/editor/editorParts.ts @@ -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 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 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 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 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 implements IEditor return this.getPart(container).createEditorDropTarget(container, delegate); } + //#endregion + + //#region Editor Group Context Key Handling + private readonly globalContextKeys = new Map>(); private readonly scopedContextKeys = new Map>>(); + 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(contextKey: RawContextKey, group: IEditorGroupView): IContextKey { // Ensure we only bind to the same context key once globaly @@ -679,27 +705,70 @@ export class EditorParts extends MultiWindowParts implements IEditor }; } - private updateGlobalContextKeys(): void { - const activeGroupScopedContextKeys = this.scopedContextKeys.get(this.activeGroup.id); - if (!activeGroupScopedContextKeys) { - return; + private readonly contextKeyProviders = new Map>(); + private readonly registeredContextKeys = new Map>(); + + registerContextKeyProvider(provider: IEditorGroupContextKeyProvider): 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()); + 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(group: IEditorGroupView, provider: IEditorGroupContextKeyProvider): 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(); + 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 diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 088eec24d7cb0..7ee1d516f6de5 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -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; - private activeResourceRepositoryContextKey: IContextKey; private readonly disposables = new DisposableStore(); private repositoryDisposables = new Set(); + private onDidRepositoryChange = new Emitter(); 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('scmActiveResourceHasChanges', false, localize('scmActiveResourceHasChanges', "Whether the active resource has changes")); + const activeResourceRepositoryContextKey = new RawContextKey('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 = { + contextKey: activeResourceHasChangesContextKey, + getGroupContextKeyValue: (group) => this.getEditorHasChanges(group.activeEditor), + onDidChange: this.onDidRepositoryChange.event + }; + + const repositoryContextKeyProvider: IEditorGroupContextKeyProvider = { + 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(); } } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 8670f77ece2be..6a7acef2ae618 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -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 { + + /** + * The context key that needs to be set for each editor group context and the global context. + */ + readonly contextKey: RawContextKey; + + /** + * 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; +} + /** * 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(provider: IEditorGroupContextKeyProvider): IDisposable; } export const enum OpenEditorContext { diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index f458db5c34f29..80455d79ac1aa 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -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(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('testContextKey', parts.activeGroup.id); + const contextKeyProvider: IEditorGroupContextKeyProvider = { + 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(); + + const rawContextKey = new RawContextKey('testContextKey', parts.activeGroup.id); + const contextKeyProvider: IEditorGroupContextKeyProvider = { + 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('testContextKey', input1.resource.toString()); + const contextKeyProvider: IEditorGroupContextKeyProvider = { + 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(); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index e8c445facde5a..cf3df1f56ce65 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -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(_provider: IEditorGroupContextKeyProvider): 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 { throw new Error('Method not implemented.'); } deleteWorkingSet(workingSet: IEditorWorkingSet): Promise { throw new Error('Method not implemented.'); } -} - -export async function createEditorPart(instantiationService: IInstantiationService, disposables: DisposableStore): Promise { - class TestEditorParts extends EditorParts { + registerContextKeyProvider(provider: IEditorGroupContextKeyProvider): IDisposable { throw new Error('Method not implemented.'); } +} - testMainPart!: TestEditorPart; +export class TestEditorParts extends EditorParts { + testMainPart!: TestEditorPart; - protected override createMainEditorPart(): MainEditorPart { - this.testMainPart = instantiationService.createInstance(TestEditorPart, this); + protected override createMainEditorPart(): MainEditorPart { + this.testMainPart = this.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 { + 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 parts; +} - return part; +export async function createEditorPart(instantiationService: IInstantiationService, disposables: DisposableStore): Promise { + return (await createEditorParts(instantiationService, disposables)).testMainPart; } export class TestListService implements IListService {