/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ViewContainer, IViewsRegistry, IViewDescriptor, Extensions as ViewExtensions, IViewContainerModel, IAddedViewDescriptorRef, IViewDescriptorRef } from 'vs/workbench/common/views'; import { IContextKeyService, IReadableSet } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; import { firstIndex, move } from 'vs/base/common/arrays'; import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; import { values } from 'vs/base/common/map'; import { isEqual } from 'vs/base/common/resources'; class CounterSet implements IReadableSet { private map = new Map(); add(value: T): CounterSet { this.map.set(value, (this.map.get(value) || 0) + 1); return this; } delete(value: T): boolean { let counter = this.map.get(value) || 0; if (counter === 0) { return false; } counter--; if (counter === 0) { this.map.delete(value); } else { this.map.set(value, counter); } return true; } has(value: T): boolean { return this.map.has(value); } } interface IStoredWorkspaceViewState { collapsed: boolean; isHidden: boolean; size?: number; order?: number; } interface IStoredGlobalViewState { id: string; isHidden: boolean; order?: number; } interface IViewDescriptorState { visibleGlobal: boolean | undefined; visibleWorkspace: boolean | undefined; collapsed: boolean | undefined; active: boolean order?: number; size?: number; } class ViewDescriptorsState extends Disposable { private readonly workspaceViewsStateStorageId: string; private readonly globalViewsStateStorageId: string; private readonly state: Map; private _onDidChangeStoredState = this._register(new Emitter<{ id: string, visible: boolean }[]>()); readonly onDidChangeStoredState = this._onDidChangeStoredState.event; constructor( viewContainerStorageId: string, @IStorageService private readonly storageService: IStorageService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService ) { super(); this.globalViewsStateStorageId = `${viewContainerStorageId}.hidden`; this.workspaceViewsStateStorageId = viewContainerStorageId; storageKeysSyncRegistryService.registerStorageKey({ key: this.globalViewsStateStorageId, version: 1 }); this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); this.state = this.initialize(); } set(id: string, state: IViewDescriptorState): void { this.state.set(id, state); } get(id: string): IViewDescriptorState | undefined { return this.state.get(id); } updateState(viewDescriptors: ReadonlyArray): void { this.updateWorkspaceState(viewDescriptors); this.updateGlobalState(viewDescriptors); } private updateWorkspaceState(viewDescriptors: ReadonlyArray): void { const storedViewsStates: { [id: string]: IStoredWorkspaceViewState; } = JSON.parse(this.storageService.get(this.workspaceViewsStateStorageId, StorageScope.WORKSPACE, '{}')); for (const viewDescriptor of viewDescriptors) { const viewState = this.state.get(viewDescriptor.id); if (viewState) { storedViewsStates[viewDescriptor.id] = { collapsed: !!viewState.collapsed, isHidden: !viewState.visibleWorkspace, size: viewState.size, order: viewDescriptor.workspace && viewState ? viewState.order : undefined }; } } if (Object.keys(storedViewsStates).length > 0) { this.storageService.store(this.workspaceViewsStateStorageId, JSON.stringify(storedViewsStates), StorageScope.WORKSPACE); } else { this.storageService.remove(this.workspaceViewsStateStorageId, StorageScope.WORKSPACE); } } private updateGlobalState(viewDescriptors: ReadonlyArray): void { const storedGlobalState = this.getStoredGlobalState(); for (const viewDescriptor of viewDescriptors) { const state = this.state.get(viewDescriptor.id); storedGlobalState.set(viewDescriptor.id, { id: viewDescriptor.id, isHidden: state && viewDescriptor.canToggleVisibility ? !state.visibleGlobal : false, order: !viewDescriptor.workspace && state ? state.order : undefined }); } this.setStoredGlobalState(storedGlobalState); } private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void { if (e.key === this.globalViewsStateStorageId && e.scope === StorageScope.GLOBAL && this.globalViewsStatesValue !== this.getStoredGlobalViewsStatesValue() /* This checks if current window changed the value or not */) { this._globalViewsStatesValue = undefined; const storedViewsVisibilityStates = this.getStoredGlobalState(); const changedStates: { id: string, visible: boolean }[] = []; for (const [id, storedState] of storedViewsVisibilityStates) { const state = this.state.get(id); if (state) { if (state.visibleGlobal !== !storedState.isHidden) { changedStates.push({ id, visible: !storedState.isHidden }); } } } if (changedStates.length) { this._onDidChangeStoredState.fire(changedStates); } } } private initialize(): Map { const viewStates = new Map(); const workspaceViewsStates = <{ [id: string]: IStoredWorkspaceViewState; }>JSON.parse(this.storageService.get(this.workspaceViewsStateStorageId, StorageScope.WORKSPACE, '{}')); for (const id of Object.keys(workspaceViewsStates)) { const workspaceViewState = workspaceViewsStates[id]; viewStates.set(id, { active: false, visibleGlobal: undefined, visibleWorkspace: isUndefined(workspaceViewState.isHidden) ? undefined : !workspaceViewState.isHidden, collapsed: workspaceViewState.collapsed, order: workspaceViewState.order, size: workspaceViewState.size, }); } // Migrate to `viewletStateStorageId` const value = this.storageService.get(this.globalViewsStateStorageId, StorageScope.WORKSPACE, '[]'); const { state: workspaceVisibilityStates } = this.parseStoredGlobalState(value); if (workspaceVisibilityStates.size > 0) { for (const { id, isHidden } of values(workspaceVisibilityStates)) { let viewState = viewStates.get(id); // Not migrated to `viewletStateStorageId` if (viewState) { if (isUndefined(viewState.visibleWorkspace)) { viewState.visibleWorkspace = !isHidden; } } else { viewStates.set(id, { active: false, collapsed: undefined, visibleGlobal: undefined, visibleWorkspace: !isHidden, }); } } this.storageService.remove(this.globalViewsStateStorageId, StorageScope.WORKSPACE); } const { state, hasDuplicates } = this.parseStoredGlobalState(this.globalViewsStatesValue); if (hasDuplicates) { this.setStoredGlobalState(state); } for (const { id, isHidden, order } of values(state)) { let viewState = viewStates.get(id); if (viewState) { viewState.visibleGlobal = !isHidden; if (!isUndefined(order)) { viewState.order = order; } } else { viewStates.set(id, { active: false, visibleGlobal: !isHidden, order, collapsed: undefined, visibleWorkspace: undefined, }); } } return viewStates; } private getStoredGlobalState(): Map { return this.parseStoredGlobalState(this.globalViewsStatesValue).state; } private setStoredGlobalState(storedGlobalState: Map): void { this.globalViewsStatesValue = JSON.stringify(values(storedGlobalState)); } private parseStoredGlobalState(value: string): { state: Map, hasDuplicates: boolean } { const storedValue = >JSON.parse(value); let hasDuplicates = false; const state = storedValue.reduce((result, storedState) => { if (typeof storedState === 'string' /* migration */) { hasDuplicates = hasDuplicates || result.has(storedState); result.set(storedState, { id: storedState, isHidden: true }); } else { hasDuplicates = hasDuplicates || result.has(storedState.id); result.set(storedState.id, storedState); } return result; }, new Map()); return { state, hasDuplicates }; } private _globalViewsStatesValue: string | undefined; private get globalViewsStatesValue(): string { if (!this._globalViewsStatesValue) { this._globalViewsStatesValue = this.getStoredGlobalViewsStatesValue(); } return this._globalViewsStatesValue; } private set globalViewsStatesValue(globalViewsStatesValue: string) { if (this.globalViewsStatesValue !== globalViewsStatesValue) { this._globalViewsStatesValue = globalViewsStatesValue; this.setStoredGlobalViewsStatesValue(globalViewsStatesValue); } } private getStoredGlobalViewsStatesValue(): string { return this.storageService.get(this.globalViewsStateStorageId, StorageScope.GLOBAL, '[]'); } private setStoredGlobalViewsStatesValue(value: string): void { this.storageService.store(this.globalViewsStateStorageId, value, StorageScope.GLOBAL); } } interface IViewDescriptorItem { viewDescriptor: IViewDescriptor; state: IViewDescriptorState; } export class ViewContainerModel extends Disposable implements IViewContainerModel { private readonly contextKeys = new CounterSet(); private viewDescriptorItems: IViewDescriptorItem[] = []; private viewDescriptorsState: ViewDescriptorsState; // Container Info private _title!: string; get title(): string { return this._title; } private _icon: URI | string | undefined; get icon(): URI | string | undefined { return this._icon; } private _onDidChangeContainerInfo = this._register(new Emitter<{ title?: boolean, icon?: boolean }>()); readonly onDidChangeContainerInfo = this._onDidChangeContainerInfo.event; // All View Descriptors get allViewDescriptors(): ReadonlyArray { return this.viewDescriptorItems.map(item => item.viewDescriptor); } private _onDidChangeAllViewDescriptors = this._register(new Emitter<{ added: ReadonlyArray, removed: ReadonlyArray }>()); readonly onDidChangeAllViewDescriptors = this._onDidChangeAllViewDescriptors.event; // Active View Descriptors get activeViewDescriptors(): ReadonlyArray { return this.viewDescriptorItems.filter(item => item.state.active).map(item => item.viewDescriptor); } private _onDidChangeActiveViewDescriptors = this._register(new Emitter<{ added: ReadonlyArray, removed: ReadonlyArray }>()); readonly onDidChangeActiveViewDescriptors = this._onDidChangeActiveViewDescriptors.event; // Visible View Descriptors get visibleViewDescriptors(): ReadonlyArray { return this.viewDescriptorItems.filter(item => this.isViewDescriptorVisible(item)).map(item => item.viewDescriptor); } private _onDidAddVisibleViewDescriptors = this._register(new Emitter()); readonly onDidAddVisibleViewDescriptors: Event = this._onDidAddVisibleViewDescriptors.event; private _onDidRemoveVisibleViewDescriptors = this._register(new Emitter()); readonly onDidRemoveVisibleViewDescriptors: Event = this._onDidRemoveVisibleViewDescriptors.event; private _onDidMoveVisibleViewDescriptors = this._register(new Emitter<{ from: IViewDescriptorRef; to: IViewDescriptorRef; }>()); readonly onDidMoveVisibleViewDescriptors: Event<{ from: IViewDescriptorRef; to: IViewDescriptorRef; }> = this._onDidMoveVisibleViewDescriptors.event; constructor( private readonly container: ViewContainer, @IInstantiationService instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); this._register(Event.filter(contextKeyService.onDidChangeContext, e => e.affectsSome(this.contextKeys))(() => this.onDidChangeContext())); this.viewDescriptorsState = this._register(instantiationService.createInstance(ViewDescriptorsState, container.storageId || `${container.id}.state`)); this._register(this.viewDescriptorsState.onDidChangeStoredState(items => this.updateVisibility(items))); this._register(Event.any( this.onDidAddVisibleViewDescriptors, this.onDidRemoveVisibleViewDescriptors, this.onDidMoveVisibleViewDescriptors) (() => { this.viewDescriptorsState.updateState(this.allViewDescriptors); this.updateContainerInfo(); })); this.updateContainerInfo(); } private updateContainerInfo(): void { /* Use default container info if one of the visible view descriptors belongs to the current container by default */ const useDefaultContainerInfo = this.container.alwaysUseContainerInfo || this.visibleViewDescriptors.length === 0 || this.visibleViewDescriptors.some(v => Registry.as(ViewExtensions.ViewsRegistry).getViewContainer(v.id) === this.container); const title = useDefaultContainerInfo ? this.container.name : this.visibleViewDescriptors[0]?.containerTitle || this.visibleViewDescriptors[0]?.name || ''; let titleChanged: boolean = false; if (this._title !== title) { this._title = title; titleChanged = true; } const icon = useDefaultContainerInfo ? this.container.icon : this.visibleViewDescriptors[0]?.containerIcon || 'codicon-window'; let iconChanged: boolean = false; if (URI.isUri(icon) && URI.isUri(this._icon) ? isEqual(icon, this._icon) : this._icon !== icon) { this._icon = icon; iconChanged = true; } if (titleChanged || iconChanged) { this._onDidChangeContainerInfo.fire({ title: titleChanged, icon: iconChanged }); } } isVisible(id: string): boolean { const viewDescriptorItem = this.viewDescriptorItems.filter(v => v.viewDescriptor.id === id)[0]; if (!viewDescriptorItem) { throw new Error(`Unknown view ${id}`); } return this.isViewDescriptorVisible(viewDescriptorItem); } setVisible(id: string, visible: boolean, size?: number): void { this.updateVisibility([{ id, visible, size }]); } private updateVisibility(viewDescriptors: { id: string, visible: boolean, size?: number }[]): void { const added: IAddedViewDescriptorRef[] = []; const removed: IViewDescriptorRef[] = []; for (const { visibleIndex, viewDescriptorItem, visible, size } of viewDescriptors.map(({ id, visible, size }) => ({ ...this.find(id), visible, size }))) { const viewDescriptor = viewDescriptorItem.viewDescriptor; if (!viewDescriptor.canToggleVisibility) { continue; } if (this.isViewDescriptorVisibleWhenActive(viewDescriptorItem) === visible) { continue; } if (viewDescriptor.workspace) { viewDescriptorItem.state.visibleWorkspace = visible; } else { viewDescriptorItem.state.visibleGlobal = visible; } if (typeof viewDescriptorItem.state.size === 'number') { viewDescriptorItem.state.size = size; } if (this.isViewDescriptorVisible(viewDescriptorItem) !== visible) { // do not add events if visibility is not changed continue; } if (visible) { added.push({ index: visibleIndex, viewDescriptor, size: viewDescriptorItem.state.size, collapsed: !!viewDescriptorItem.state.collapsed }); } else { removed.push({ index: visibleIndex, viewDescriptor }); } } if (added.length) { this._onDidAddVisibleViewDescriptors.fire(added); } if (removed.length) { this._onDidRemoveVisibleViewDescriptors.fire(removed); } } isCollapsed(id: string): boolean { return !!this.find(id).viewDescriptorItem.state.collapsed; } setCollapsed(id: string, collapsed: boolean): void { const { viewDescriptorItem } = this.find(id); if (viewDescriptorItem.state.collapsed !== collapsed) { viewDescriptorItem.state.collapsed = collapsed; } this.viewDescriptorsState.updateState(this.allViewDescriptors); } getSize(id: string): number | undefined { return this.find(id).viewDescriptorItem.state.size; } setSize(id: string, size: number): void { const { viewDescriptorItem } = this.find(id); if (viewDescriptorItem.state.size !== size) { viewDescriptorItem.state.size = size; } this.viewDescriptorsState.updateState(this.allViewDescriptors); } move(from: string, to: string): void { const fromIndex = firstIndex(this.viewDescriptorItems, v => v.viewDescriptor.id === from); const toIndex = firstIndex(this.viewDescriptorItems, v => v.viewDescriptor.id === to); const fromViewDescriptor = this.viewDescriptorItems[fromIndex]; const toViewDescriptor = this.viewDescriptorItems[toIndex]; move(this.viewDescriptorItems, fromIndex, toIndex); for (let index = 0; index < this.viewDescriptorItems.length; index++) { this.viewDescriptorItems[index].state.order = index; } this._onDidMoveVisibleViewDescriptors.fire({ from: { index: fromIndex, viewDescriptor: fromViewDescriptor.viewDescriptor }, to: { index: toIndex, viewDescriptor: toViewDescriptor.viewDescriptor } }); } add(viewDescriptors: IViewDescriptor[]): void { const addedItems: IViewDescriptorItem[] = []; const addedActiveDescriptors: IViewDescriptor[] = []; const addedVisibleItems: { index: number, viewDescriptor: IViewDescriptor, size?: number, collapsed: boolean; }[] = []; for (const viewDescriptor of viewDescriptors) { if (viewDescriptor.when) { for (const key of viewDescriptor.when.keys()) { this.contextKeys.add(key); } } let state = this.viewDescriptorsState.get(viewDescriptor.id); if (state) { // set defaults if not set if (viewDescriptor.workspace) { state.visibleWorkspace = isUndefinedOrNull(state.visibleWorkspace) ? !viewDescriptor.hideByDefault : state.visibleWorkspace; } else { state.visibleGlobal = isUndefinedOrNull(state.visibleGlobal) ? !viewDescriptor.hideByDefault : state.visibleGlobal; } state.collapsed = isUndefinedOrNull(state.collapsed) ? !!viewDescriptor.collapsed : state.collapsed; } else { state = { active: false, visibleGlobal: !viewDescriptor.hideByDefault, visibleWorkspace: !viewDescriptor.hideByDefault, collapsed: !!viewDescriptor.collapsed, }; } this.viewDescriptorsState.set(viewDescriptor.id, state); state.active = this.contextKeyService.contextMatchesRules(viewDescriptor.when); addedItems.push({ viewDescriptor, state }); if (state.active) { addedActiveDescriptors.push(viewDescriptor); } } this.viewDescriptorItems.push(...addedItems); this.viewDescriptorItems.sort(this.compareViewDescriptors.bind(this)); for (const viewDescriptorItem of addedItems) { if (this.isViewDescriptorVisible(viewDescriptorItem)) { const { visibleIndex } = this.find(viewDescriptorItem.viewDescriptor.id); addedVisibleItems.push({ index: visibleIndex, viewDescriptor: viewDescriptorItem.viewDescriptor, size: viewDescriptorItem.state.size, collapsed: !!viewDescriptorItem.state.collapsed }); } } this._onDidChangeAllViewDescriptors.fire({ added: addedItems.map(({ viewDescriptor }) => viewDescriptor), removed: [] }); if (addedActiveDescriptors.length) { this._onDidChangeActiveViewDescriptors.fire(({ added: addedActiveDescriptors, removed: [] })); } if (addedVisibleItems.length) { this._onDidAddVisibleViewDescriptors.fire(addedVisibleItems); } } remove(viewDescriptors: IViewDescriptor[]): void { const removed: IViewDescriptor[] = []; const removedItems: IViewDescriptorItem[] = []; const removedActiveDescriptors: IViewDescriptor[] = []; const removedVisibleItems: { index: number, viewDescriptor: IViewDescriptor; }[] = []; for (const viewDescriptor of viewDescriptors) { if (viewDescriptor.when) { for (const key of viewDescriptor.when.keys()) { this.contextKeys.delete(key); } } const index = firstIndex(this.viewDescriptorItems, i => i.viewDescriptor.id === viewDescriptor.id); if (index !== -1) { removed.push(viewDescriptor); const viewDescriptorItem = this.viewDescriptorItems[index]; if (viewDescriptorItem.state.active) { removedActiveDescriptors.push(viewDescriptorItem.viewDescriptor); } if (this.isViewDescriptorVisible(viewDescriptorItem)) { const { visibleIndex } = this.find(viewDescriptorItem.viewDescriptor.id); removedVisibleItems.push({ index: visibleIndex, viewDescriptor: viewDescriptorItem.viewDescriptor }); } removedItems.push(viewDescriptorItem); } } removedItems.forEach(item => this.viewDescriptorItems.splice(this.viewDescriptorItems.indexOf(item), 1)); this._onDidChangeAllViewDescriptors.fire({ added: [], removed }); if (removedActiveDescriptors.length) { this._onDidChangeActiveViewDescriptors.fire(({ added: [], removed: removedActiveDescriptors })); } if (removedVisibleItems.length) { this._onDidRemoveVisibleViewDescriptors.fire(removedVisibleItems); } } private onDidChangeContext(): void { const addedActiveItems: { item: IViewDescriptorItem, wasVisible: boolean }[] = []; const removedActiveItems: { item: IViewDescriptorItem, wasVisible: boolean }[] = []; const removedVisibleItems: { index: number, viewDescriptor: IViewDescriptor; }[] = []; const addedVisibleItems: { index: number, viewDescriptor: IViewDescriptor, size?: number, collapsed: boolean; }[] = []; for (const item of this.viewDescriptorItems) { const wasActive = item.state.active; const wasVisible = this.isViewDescriptorVisible(item); const isActive = this.contextKeyService.contextMatchesRules(item.viewDescriptor.when); if (wasActive !== isActive) { if (isActive) { addedActiveItems.push({ item, wasVisible }); } else { removedActiveItems.push({ item, wasVisible }); } } } for (const { item, wasVisible } of removedActiveItems) { if (wasVisible) { const { visibleIndex } = this.find(item.viewDescriptor.id); removedVisibleItems.push({ index: visibleIndex, viewDescriptor: item.viewDescriptor }); } } // Update the State removedActiveItems.forEach(({ item }) => item.state.active = false); addedActiveItems.forEach(({ item }) => item.state.active = true); for (const { item, wasVisible } of addedActiveItems) { if (wasVisible !== this.isViewDescriptorVisibleWhenActive(item)) { const { visibleIndex } = this.find(item.viewDescriptor.id); addedVisibleItems.push({ index: visibleIndex, viewDescriptor: item.viewDescriptor, size: item.state.size, collapsed: !!item.state.collapsed }); } } if (addedActiveItems.length || removedActiveItems.length) { this._onDidChangeActiveViewDescriptors.fire(({ added: addedActiveItems.map(({ item }) => item.viewDescriptor), removed: removedActiveItems.map(({ item }) => item.viewDescriptor) })); } if (removedVisibleItems.length) { this._onDidRemoveVisibleViewDescriptors.fire(removedVisibleItems); } if (addedVisibleItems.length) { this._onDidAddVisibleViewDescriptors.fire(addedVisibleItems); } } private isViewDescriptorVisible(viewDescriptorItem: IViewDescriptorItem): boolean { if (!viewDescriptorItem.state.active) { return false; } return this.isViewDescriptorVisibleWhenActive(viewDescriptorItem); } private isViewDescriptorVisibleWhenActive(viewDescriptorItem: IViewDescriptorItem): boolean { if (viewDescriptorItem.viewDescriptor.workspace) { return !!viewDescriptorItem.state.visibleWorkspace; } return !!viewDescriptorItem.state.visibleGlobal; } private find(id: string): { index: number, visibleIndex: number, viewDescriptorItem: IViewDescriptorItem; } { for (let i = 0, visibleIndex = 0; i < this.viewDescriptorItems.length; i++) { const viewDescriptorItem = this.viewDescriptorItems[i]; if (viewDescriptorItem.viewDescriptor.id === id) { return { index: i, visibleIndex, viewDescriptorItem: viewDescriptorItem }; } if (this.isViewDescriptorVisible(viewDescriptorItem)) { visibleIndex++; } } throw new Error(`view descriptor ${id} not found`); } private compareViewDescriptors(a: IViewDescriptorItem, b: IViewDescriptorItem): number { if (a.viewDescriptor.id === b.viewDescriptor.id) { return 0; } return (this.getViewOrder(a) - this.getViewOrder(b)) || this.getGroupOrderResult(a.viewDescriptor, b.viewDescriptor); } private getViewOrder(viewDescriptorItem: IViewDescriptorItem): number { const viewOrder = typeof viewDescriptorItem.state.order === 'number' ? viewDescriptorItem.state.order : viewDescriptorItem.viewDescriptor.order; return typeof viewOrder === 'number' ? viewOrder : Number.MAX_VALUE; } private getGroupOrderResult(a: IViewDescriptor, b: IViewDescriptor) { if (!a.group || !b.group) { return 0; } if (a.group === b.group) { return 0; } return a.group < b.group ? -1 : 1; } }