Path: blob/main/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts
5310 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Disposable } from '../../../../base/common/lifecycle.js';6import { isStatusbarEntryLocation, IStatusbarEntryPriority, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js';7import { hide, show, isAncestorOfActiveElement } from '../../../../base/browser/dom.js';8import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';9import { Emitter } from '../../../../base/common/event.js';1011export interface IStatusbarViewModelEntry {12readonly id: string;13readonly extensionId: string | undefined;14readonly name: string;15readonly hasCommand: boolean;16readonly alignment: StatusbarAlignment;17readonly priority: IStatusbarEntryPriority;18readonly container: HTMLElement;19readonly labelContainer: HTMLElement;20}2122export class StatusbarViewModel extends Disposable {2324private static readonly HIDDEN_ENTRIES_KEY = 'workbench.statusbar.hidden';2526private readonly _onDidChangeEntryVisibility = this._register(new Emitter<{ id: string; visible: boolean }>());27readonly onDidChangeEntryVisibility = this._onDidChangeEntryVisibility.event;2829private _entries: IStatusbarViewModelEntry[] = []; // Intentionally not using a map here since multiple entries can have the same ID30get entries(): IStatusbarViewModelEntry[] { return this._entries.slice(0); }3132private _lastFocusedEntry: IStatusbarViewModelEntry | undefined;33get lastFocusedEntry(): IStatusbarViewModelEntry | undefined {34return this._lastFocusedEntry && !this.isHidden(this._lastFocusedEntry.id) ? this._lastFocusedEntry : undefined;35}3637private hidden = new Set<string>();3839constructor(private readonly storageService: IStorageService) {40super();4142this.restoreState();43this.registerListeners();44}4546private restoreState(): void {47const hiddenRaw = this.storageService.get(StatusbarViewModel.HIDDEN_ENTRIES_KEY, StorageScope.PROFILE);48if (hiddenRaw) {49try {50this.hidden = new Set(JSON.parse(hiddenRaw));51} catch (error) {52// ignore parsing errors53}54}55}5657private registerListeners(): void {58this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, StatusbarViewModel.HIDDEN_ENTRIES_KEY, this._store)(() => this.onDidStorageValueChange()));59}6061private onDidStorageValueChange(): void {6263// Keep current hidden entries64const currentlyHidden = new Set(this.hidden);6566// Load latest state of hidden entries67this.hidden.clear();68this.restoreState();6970const changed = new Set<string>();7172// Check for each entry that is now visible73for (const id of currentlyHidden) {74if (!this.hidden.has(id)) {75changed.add(id);76}77}7879// Check for each entry that is now hidden80for (const id of this.hidden) {81if (!currentlyHidden.has(id)) {82changed.add(id);83}84}8586// Update visibility for entries have changed87if (changed.size > 0) {88for (const entry of this._entries) {89if (changed.has(entry.id)) {90this.updateVisibility(entry.id, true);9192changed.delete(entry.id);93}94}95}96}9798add(entry: IStatusbarViewModelEntry): void {99100// Add to set of entries101this._entries.push(entry);102103// Update visibility directly104this.updateVisibility(entry, false);105106// Sort according to priority107this.sort();108109// Mark first/last visible entry110this.markFirstLastVisibleEntry();111}112113remove(entry: IStatusbarViewModelEntry): void {114const index = this._entries.indexOf(entry);115if (index >= 0) {116117// Remove from entries118this._entries.splice(index, 1);119120// Re-sort entries if this one was used121// as reference from other entries122if (this._entries.some(otherEntry => isStatusbarEntryLocation(otherEntry.priority.primary) && otherEntry.priority.primary.location.id === entry.id)) {123this.sort();124}125126// Mark first/last visible entry127this.markFirstLastVisibleEntry();128}129}130131isHidden(id: string): boolean {132return this.hidden.has(id);133}134135hide(id: string): void {136if (!this.hidden.has(id)) {137this.hidden.add(id);138139this.updateVisibility(id, true);140141this.saveState();142}143}144145show(id: string): void {146if (this.hidden.delete(id)) {147this.updateVisibility(id, true);148149this.saveState();150}151}152153findEntry(container: HTMLElement): IStatusbarViewModelEntry | undefined {154return this._entries.find(entry => entry.container === container);155}156157getEntries(alignment: StatusbarAlignment): IStatusbarViewModelEntry[] {158return this._entries.filter(entry => entry.alignment === alignment);159}160161focusNextEntry(): void {162this.focusEntry(+1, 0);163}164165focusPreviousEntry(): void {166this.focusEntry(-1, this.entries.length - 1);167}168169isEntryFocused(): boolean {170return !!this.getFocusedEntry();171}172173private getFocusedEntry(): IStatusbarViewModelEntry | undefined {174return this._entries.find(entry => isAncestorOfActiveElement(entry.container));175}176177private focusEntry(delta: number, restartPosition: number): void {178179const getVisibleEntry = (start: number) => {180let indexToFocus = start;181let entry = (indexToFocus >= 0 && indexToFocus < this._entries.length) ? this._entries[indexToFocus] : undefined;182while (entry && this.isHidden(entry.id)) {183indexToFocus += delta;184entry = (indexToFocus >= 0 && indexToFocus < this._entries.length) ? this._entries[indexToFocus] : undefined;185}186187return entry;188};189190const focused = this.getFocusedEntry();191if (focused) {192const entry = getVisibleEntry(this._entries.indexOf(focused) + delta);193if (entry) {194this._lastFocusedEntry = entry;195196entry.labelContainer.focus();197198return;199}200}201202const entry = getVisibleEntry(restartPosition);203if (entry) {204this._lastFocusedEntry = entry;205entry.labelContainer.focus();206}207}208209private updateVisibility(id: string, trigger: boolean): void;210private updateVisibility(entry: IStatusbarViewModelEntry, trigger: boolean): void;211private updateVisibility(arg1: string | IStatusbarViewModelEntry, trigger: boolean): void {212213// By identifier214if (typeof arg1 === 'string') {215const id = arg1;216217for (const entry of this._entries) {218if (entry.id === id) {219this.updateVisibility(entry, trigger);220}221}222}223224// By entry225else {226const entry = arg1;227const isHidden = this.isHidden(entry.id);228229// Use CSS to show/hide item container230if (isHidden) {231hide(entry.container);232} else {233show(entry.container);234}235236if (trigger) {237this._onDidChangeEntryVisibility.fire({ id: entry.id, visible: !isHidden });238}239240// Mark first/last visible entry241this.markFirstLastVisibleEntry();242}243}244245private saveState(): void {246if (this.hidden.size > 0) {247this.storageService.store(StatusbarViewModel.HIDDEN_ENTRIES_KEY, JSON.stringify(Array.from(this.hidden.values())), StorageScope.PROFILE, StorageTarget.USER);248} else {249this.storageService.remove(StatusbarViewModel.HIDDEN_ENTRIES_KEY, StorageScope.PROFILE);250}251}252253private sort(): void {254const allEntryIds = new Set(this._entries.map(entry => entry.id));255256// Split up entries into 2 buckets:257// - those with priority as number that can be compared or with a missing relative entry258// - those with a relative priority that must be sorted relative to another entry that exists259const mapEntryWithNumberedPriorityToIndex = new Map<IStatusbarViewModelEntry, number /* priority of entry as number */>();260const mapEntryWithRelativePriority = new Map<string /* id of entry to position after */, Map<string, IStatusbarViewModelEntry>>();261for (let i = 0; i < this._entries.length; i++) {262const entry = this._entries[i];263if (typeof entry.priority.primary === 'number' || !allEntryIds.has(entry.priority.primary.location.id)) {264mapEntryWithNumberedPriorityToIndex.set(entry, i);265} else {266const referenceEntryId = entry.priority.primary.location.id;267let entries = mapEntryWithRelativePriority.get(referenceEntryId);268if (!entries) {269270// It is possible that this entry references another entry271// that itself references an entry. In that case, we want272// to add it to the entries of the referenced entry.273274for (const relativeEntries of mapEntryWithRelativePriority.values()) {275if (relativeEntries.has(referenceEntryId)) {276entries = relativeEntries;277break;278}279}280281if (!entries) {282entries = new Map();283mapEntryWithRelativePriority.set(referenceEntryId, entries);284}285}286entries.set(entry.id, entry);287}288}289290// Sort the entries with `priority: number` or referencing a missing entry accordingly291const sortedEntriesWithNumberedPriority = Array.from(mapEntryWithNumberedPriorityToIndex.keys());292sortedEntriesWithNumberedPriority.sort((entryA, entryB) => {293if (entryA.alignment === entryB.alignment) {294295// Sort by primary/secondary priority: higher values move towards the left296297const entryAPrimaryPriority = typeof entryA.priority.primary === 'number' ? entryA.priority.primary : entryA.priority.primary.location.priority;298const entryBPrimaryPriority = typeof entryB.priority.primary === 'number' ? entryB.priority.primary : entryB.priority.primary.location.priority;299300if (entryAPrimaryPriority !== entryBPrimaryPriority) {301return entryBPrimaryPriority - entryAPrimaryPriority;302}303304if (entryA.priority.secondary !== entryB.priority.secondary) {305return entryB.priority.secondary - entryA.priority.secondary;306}307308// otherwise maintain stable order (both values known to be in map)309return mapEntryWithNumberedPriorityToIndex.get(entryA)! - mapEntryWithNumberedPriorityToIndex.get(entryB)!;310}311312if (entryA.alignment === StatusbarAlignment.LEFT) {313return -1;314}315316if (entryB.alignment === StatusbarAlignment.LEFT) {317return 1;318}319320return 0;321});322323let sortedEntries: IStatusbarViewModelEntry[];324325// Entries with location: sort in accordingly326if (mapEntryWithRelativePriority.size > 0) {327sortedEntries = [];328329for (const entry of sortedEntriesWithNumberedPriority) {330const relativeEntriesMap = mapEntryWithRelativePriority.get(entry.id);331const relativeEntries = relativeEntriesMap ? Array.from(relativeEntriesMap.values()) : undefined;332333// Fill relative entries to LEFT334if (relativeEntries) {335sortedEntries.push(...relativeEntries336.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.LEFT)337.sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));338}339340// Fill referenced entry341sortedEntries.push(entry);342343// Fill relative entries to RIGHT344if (relativeEntries) {345sortedEntries.push(...relativeEntries346.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.RIGHT)347.sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));348}349350// Delete from map to mark as handled351mapEntryWithRelativePriority.delete(entry.id);352}353354// Finally, just append all entries that reference another entry355// that does not exist to the end of the list356//357// Note: this should really not happen because of our check in358// `allEntryIds`, but we play it safe here to really consume359// all entries.360//361for (const [, entries] of mapEntryWithRelativePriority) {362sortedEntries.push(...Array.from(entries.values()).sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));363}364}365366// No entries with relative priority: take sorted entries as is367else {368sortedEntries = sortedEntriesWithNumberedPriority;369}370371// Take over as new truth of entries372this._entries = sortedEntries;373}374375private markFirstLastVisibleEntry(): void {376this.doMarkFirstLastVisibleStatusbarItem(this.getEntries(StatusbarAlignment.LEFT));377this.doMarkFirstLastVisibleStatusbarItem(this.getEntries(StatusbarAlignment.RIGHT));378}379380private doMarkFirstLastVisibleStatusbarItem(entries: IStatusbarViewModelEntry[]): void {381let firstVisibleItem: IStatusbarViewModelEntry | undefined;382let lastVisibleItem: IStatusbarViewModelEntry | undefined;383384for (const entry of entries) {385386// Clear previous first387entry.container.classList.remove('first-visible-item', 'last-visible-item');388389const isVisible = !this.isHidden(entry.id);390if (isVisible) {391if (!firstVisibleItem) {392firstVisibleItem = entry;393}394395lastVisibleItem = entry;396}397}398399// Mark: first visible item400firstVisibleItem?.container.classList.add('first-visible-item');401402// Mark: last visible item403lastVisibleItem?.container.classList.add('last-visible-item');404}405}406407408