Path: blob/main/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts
3296 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.has(id)) {147this.hidden.delete(id);148149this.updateVisibility(id, true);150151this.saveState();152}153}154155findEntry(container: HTMLElement): IStatusbarViewModelEntry | undefined {156return this._entries.find(entry => entry.container === container);157}158159getEntries(alignment: StatusbarAlignment): IStatusbarViewModelEntry[] {160return this._entries.filter(entry => entry.alignment === alignment);161}162163focusNextEntry(): void {164this.focusEntry(+1, 0);165}166167focusPreviousEntry(): void {168this.focusEntry(-1, this.entries.length - 1);169}170171isEntryFocused(): boolean {172return !!this.getFocusedEntry();173}174175private getFocusedEntry(): IStatusbarViewModelEntry | undefined {176return this._entries.find(entry => isAncestorOfActiveElement(entry.container));177}178179private focusEntry(delta: number, restartPosition: number): void {180181const getVisibleEntry = (start: number) => {182let indexToFocus = start;183let entry = (indexToFocus >= 0 && indexToFocus < this._entries.length) ? this._entries[indexToFocus] : undefined;184while (entry && this.isHidden(entry.id)) {185indexToFocus += delta;186entry = (indexToFocus >= 0 && indexToFocus < this._entries.length) ? this._entries[indexToFocus] : undefined;187}188189return entry;190};191192const focused = this.getFocusedEntry();193if (focused) {194const entry = getVisibleEntry(this._entries.indexOf(focused) + delta);195if (entry) {196this._lastFocusedEntry = entry;197198entry.labelContainer.focus();199200return;201}202}203204const entry = getVisibleEntry(restartPosition);205if (entry) {206this._lastFocusedEntry = entry;207entry.labelContainer.focus();208}209}210211private updateVisibility(id: string, trigger: boolean): void;212private updateVisibility(entry: IStatusbarViewModelEntry, trigger: boolean): void;213private updateVisibility(arg1: string | IStatusbarViewModelEntry, trigger: boolean): void {214215// By identifier216if (typeof arg1 === 'string') {217const id = arg1;218219for (const entry of this._entries) {220if (entry.id === id) {221this.updateVisibility(entry, trigger);222}223}224}225226// By entry227else {228const entry = arg1;229const isHidden = this.isHidden(entry.id);230231// Use CSS to show/hide item container232if (isHidden) {233hide(entry.container);234} else {235show(entry.container);236}237238if (trigger) {239this._onDidChangeEntryVisibility.fire({ id: entry.id, visible: !isHidden });240}241242// Mark first/last visible entry243this.markFirstLastVisibleEntry();244}245}246247private saveState(): void {248if (this.hidden.size > 0) {249this.storageService.store(StatusbarViewModel.HIDDEN_ENTRIES_KEY, JSON.stringify(Array.from(this.hidden.values())), StorageScope.PROFILE, StorageTarget.USER);250} else {251this.storageService.remove(StatusbarViewModel.HIDDEN_ENTRIES_KEY, StorageScope.PROFILE);252}253}254255private sort(): void {256const allEntryIds = new Set(this._entries.map(entry => entry.id));257258// Split up entries into 2 buckets:259// - those with priority as number that can be compared or with a missing relative entry260// - those with a relative priority that must be sorted relative to another entry that exists261const mapEntryWithNumberedPriorityToIndex = new Map<IStatusbarViewModelEntry, number /* priority of entry as number */>();262const mapEntryWithRelativePriority = new Map<string /* id of entry to position after */, Map<string, IStatusbarViewModelEntry>>();263for (let i = 0; i < this._entries.length; i++) {264const entry = this._entries[i];265if (typeof entry.priority.primary === 'number' || !allEntryIds.has(entry.priority.primary.location.id)) {266mapEntryWithNumberedPriorityToIndex.set(entry, i);267} else {268const referenceEntryId = entry.priority.primary.location.id;269let entries = mapEntryWithRelativePriority.get(referenceEntryId);270if (!entries) {271272// It is possible that this entry references another entry273// that itself references an entry. In that case, we want274// to add it to the entries of the referenced entry.275276for (const relativeEntries of mapEntryWithRelativePriority.values()) {277if (relativeEntries.has(referenceEntryId)) {278entries = relativeEntries;279break;280}281}282283if (!entries) {284entries = new Map();285mapEntryWithRelativePriority.set(referenceEntryId, entries);286}287}288entries.set(entry.id, entry);289}290}291292// Sort the entries with `priority: number` or referencing a missing entry accordingly293const sortedEntriesWithNumberedPriority = Array.from(mapEntryWithNumberedPriorityToIndex.keys());294sortedEntriesWithNumberedPriority.sort((entryA, entryB) => {295if (entryA.alignment === entryB.alignment) {296297// Sort by primary/secondary priority: higher values move towards the left298299const entryAPrimaryPriority = typeof entryA.priority.primary === 'number' ? entryA.priority.primary : entryA.priority.primary.location.priority;300const entryBPrimaryPriority = typeof entryB.priority.primary === 'number' ? entryB.priority.primary : entryB.priority.primary.location.priority;301302if (entryAPrimaryPriority !== entryBPrimaryPriority) {303return entryBPrimaryPriority - entryAPrimaryPriority;304}305306if (entryA.priority.secondary !== entryB.priority.secondary) {307return entryB.priority.secondary - entryA.priority.secondary;308}309310// otherwise maintain stable order (both values known to be in map)311return mapEntryWithNumberedPriorityToIndex.get(entryA)! - mapEntryWithNumberedPriorityToIndex.get(entryB)!;312}313314if (entryA.alignment === StatusbarAlignment.LEFT) {315return -1;316}317318if (entryB.alignment === StatusbarAlignment.LEFT) {319return 1;320}321322return 0;323});324325let sortedEntries: IStatusbarViewModelEntry[];326327// Entries with location: sort in accordingly328if (mapEntryWithRelativePriority.size > 0) {329sortedEntries = [];330331for (const entry of sortedEntriesWithNumberedPriority) {332const relativeEntriesMap = mapEntryWithRelativePriority.get(entry.id);333const relativeEntries = relativeEntriesMap ? Array.from(relativeEntriesMap.values()) : undefined;334335// Fill relative entries to LEFT336if (relativeEntries) {337sortedEntries.push(...relativeEntries338.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.LEFT)339.sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));340}341342// Fill referenced entry343sortedEntries.push(entry);344345// Fill relative entries to RIGHT346if (relativeEntries) {347sortedEntries.push(...relativeEntries348.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.RIGHT)349.sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));350}351352// Delete from map to mark as handled353mapEntryWithRelativePriority.delete(entry.id);354}355356// Finally, just append all entries that reference another entry357// that does not exist to the end of the list358//359// Note: this should really not happen because of our check in360// `allEntryIds`, but we play it safe here to really consume361// all entries.362//363for (const [, entries] of mapEntryWithRelativePriority) {364sortedEntries.push(...Array.from(entries.values()).sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));365}366}367368// No entries with relative priority: take sorted entries as is369else {370sortedEntries = sortedEntriesWithNumberedPriority;371}372373// Take over as new truth of entries374this._entries = sortedEntries;375}376377private markFirstLastVisibleEntry(): void {378this.doMarkFirstLastVisibleStatusbarItem(this.getEntries(StatusbarAlignment.LEFT));379this.doMarkFirstLastVisibleStatusbarItem(this.getEntries(StatusbarAlignment.RIGHT));380}381382private doMarkFirstLastVisibleStatusbarItem(entries: IStatusbarViewModelEntry[]): void {383let firstVisibleItem: IStatusbarViewModelEntry | undefined;384let lastVisibleItem: IStatusbarViewModelEntry | undefined;385386for (const entry of entries) {387388// Clear previous first389entry.container.classList.remove('first-visible-item', 'last-visible-item');390391const isVisible = !this.isHidden(entry.id);392if (isVisible) {393if (!firstVisibleItem) {394firstVisibleItem = entry;395}396397lastVisibleItem = entry;398}399}400401// Mark: first visible item402firstVisibleItem?.container.classList.add('first-visible-item');403404// Mark: last visible item405lastVisibleItem?.container.classList.add('last-visible-item');406}407}408409410