Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Disposable } from '../../../../base/common/lifecycle.js';
7
import { isStatusbarEntryLocation, IStatusbarEntryPriority, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js';
8
import { hide, show, isAncestorOfActiveElement } from '../../../../base/browser/dom.js';
9
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
10
import { Emitter } from '../../../../base/common/event.js';
11
12
export interface IStatusbarViewModelEntry {
13
readonly id: string;
14
readonly extensionId: string | undefined;
15
readonly name: string;
16
readonly hasCommand: boolean;
17
readonly alignment: StatusbarAlignment;
18
readonly priority: IStatusbarEntryPriority;
19
readonly container: HTMLElement;
20
readonly labelContainer: HTMLElement;
21
}
22
23
export class StatusbarViewModel extends Disposable {
24
25
private static readonly HIDDEN_ENTRIES_KEY = 'workbench.statusbar.hidden';
26
27
private readonly _onDidChangeEntryVisibility = this._register(new Emitter<{ id: string; visible: boolean }>());
28
readonly onDidChangeEntryVisibility = this._onDidChangeEntryVisibility.event;
29
30
private _entries: IStatusbarViewModelEntry[] = []; // Intentionally not using a map here since multiple entries can have the same ID
31
get entries(): IStatusbarViewModelEntry[] { return this._entries.slice(0); }
32
33
private _lastFocusedEntry: IStatusbarViewModelEntry | undefined;
34
get lastFocusedEntry(): IStatusbarViewModelEntry | undefined {
35
return this._lastFocusedEntry && !this.isHidden(this._lastFocusedEntry.id) ? this._lastFocusedEntry : undefined;
36
}
37
38
private hidden = new Set<string>();
39
40
constructor(private readonly storageService: IStorageService) {
41
super();
42
43
this.restoreState();
44
this.registerListeners();
45
}
46
47
private restoreState(): void {
48
const hiddenRaw = this.storageService.get(StatusbarViewModel.HIDDEN_ENTRIES_KEY, StorageScope.PROFILE);
49
if (hiddenRaw) {
50
try {
51
this.hidden = new Set(JSON.parse(hiddenRaw));
52
} catch (error) {
53
// ignore parsing errors
54
}
55
}
56
}
57
58
private registerListeners(): void {
59
this._register(this.storageService.onDidChangeValue(StorageScope.PROFILE, StatusbarViewModel.HIDDEN_ENTRIES_KEY, this._store)(() => this.onDidStorageValueChange()));
60
}
61
62
private onDidStorageValueChange(): void {
63
64
// Keep current hidden entries
65
const currentlyHidden = new Set(this.hidden);
66
67
// Load latest state of hidden entries
68
this.hidden.clear();
69
this.restoreState();
70
71
const changed = new Set<string>();
72
73
// Check for each entry that is now visible
74
for (const id of currentlyHidden) {
75
if (!this.hidden.has(id)) {
76
changed.add(id);
77
}
78
}
79
80
// Check for each entry that is now hidden
81
for (const id of this.hidden) {
82
if (!currentlyHidden.has(id)) {
83
changed.add(id);
84
}
85
}
86
87
// Update visibility for entries have changed
88
if (changed.size > 0) {
89
for (const entry of this._entries) {
90
if (changed.has(entry.id)) {
91
this.updateVisibility(entry.id, true);
92
93
changed.delete(entry.id);
94
}
95
}
96
}
97
}
98
99
add(entry: IStatusbarViewModelEntry): void {
100
101
// Add to set of entries
102
this._entries.push(entry);
103
104
// Update visibility directly
105
this.updateVisibility(entry, false);
106
107
// Sort according to priority
108
this.sort();
109
110
// Mark first/last visible entry
111
this.markFirstLastVisibleEntry();
112
}
113
114
remove(entry: IStatusbarViewModelEntry): void {
115
const index = this._entries.indexOf(entry);
116
if (index >= 0) {
117
118
// Remove from entries
119
this._entries.splice(index, 1);
120
121
// Re-sort entries if this one was used
122
// as reference from other entries
123
if (this._entries.some(otherEntry => isStatusbarEntryLocation(otherEntry.priority.primary) && otherEntry.priority.primary.location.id === entry.id)) {
124
this.sort();
125
}
126
127
// Mark first/last visible entry
128
this.markFirstLastVisibleEntry();
129
}
130
}
131
132
isHidden(id: string): boolean {
133
return this.hidden.has(id);
134
}
135
136
hide(id: string): void {
137
if (!this.hidden.has(id)) {
138
this.hidden.add(id);
139
140
this.updateVisibility(id, true);
141
142
this.saveState();
143
}
144
}
145
146
show(id: string): void {
147
if (this.hidden.has(id)) {
148
this.hidden.delete(id);
149
150
this.updateVisibility(id, true);
151
152
this.saveState();
153
}
154
}
155
156
findEntry(container: HTMLElement): IStatusbarViewModelEntry | undefined {
157
return this._entries.find(entry => entry.container === container);
158
}
159
160
getEntries(alignment: StatusbarAlignment): IStatusbarViewModelEntry[] {
161
return this._entries.filter(entry => entry.alignment === alignment);
162
}
163
164
focusNextEntry(): void {
165
this.focusEntry(+1, 0);
166
}
167
168
focusPreviousEntry(): void {
169
this.focusEntry(-1, this.entries.length - 1);
170
}
171
172
isEntryFocused(): boolean {
173
return !!this.getFocusedEntry();
174
}
175
176
private getFocusedEntry(): IStatusbarViewModelEntry | undefined {
177
return this._entries.find(entry => isAncestorOfActiveElement(entry.container));
178
}
179
180
private focusEntry(delta: number, restartPosition: number): void {
181
182
const getVisibleEntry = (start: number) => {
183
let indexToFocus = start;
184
let entry = (indexToFocus >= 0 && indexToFocus < this._entries.length) ? this._entries[indexToFocus] : undefined;
185
while (entry && this.isHidden(entry.id)) {
186
indexToFocus += delta;
187
entry = (indexToFocus >= 0 && indexToFocus < this._entries.length) ? this._entries[indexToFocus] : undefined;
188
}
189
190
return entry;
191
};
192
193
const focused = this.getFocusedEntry();
194
if (focused) {
195
const entry = getVisibleEntry(this._entries.indexOf(focused) + delta);
196
if (entry) {
197
this._lastFocusedEntry = entry;
198
199
entry.labelContainer.focus();
200
201
return;
202
}
203
}
204
205
const entry = getVisibleEntry(restartPosition);
206
if (entry) {
207
this._lastFocusedEntry = entry;
208
entry.labelContainer.focus();
209
}
210
}
211
212
private updateVisibility(id: string, trigger: boolean): void;
213
private updateVisibility(entry: IStatusbarViewModelEntry, trigger: boolean): void;
214
private updateVisibility(arg1: string | IStatusbarViewModelEntry, trigger: boolean): void {
215
216
// By identifier
217
if (typeof arg1 === 'string') {
218
const id = arg1;
219
220
for (const entry of this._entries) {
221
if (entry.id === id) {
222
this.updateVisibility(entry, trigger);
223
}
224
}
225
}
226
227
// By entry
228
else {
229
const entry = arg1;
230
const isHidden = this.isHidden(entry.id);
231
232
// Use CSS to show/hide item container
233
if (isHidden) {
234
hide(entry.container);
235
} else {
236
show(entry.container);
237
}
238
239
if (trigger) {
240
this._onDidChangeEntryVisibility.fire({ id: entry.id, visible: !isHidden });
241
}
242
243
// Mark first/last visible entry
244
this.markFirstLastVisibleEntry();
245
}
246
}
247
248
private saveState(): void {
249
if (this.hidden.size > 0) {
250
this.storageService.store(StatusbarViewModel.HIDDEN_ENTRIES_KEY, JSON.stringify(Array.from(this.hidden.values())), StorageScope.PROFILE, StorageTarget.USER);
251
} else {
252
this.storageService.remove(StatusbarViewModel.HIDDEN_ENTRIES_KEY, StorageScope.PROFILE);
253
}
254
}
255
256
private sort(): void {
257
const allEntryIds = new Set(this._entries.map(entry => entry.id));
258
259
// Split up entries into 2 buckets:
260
// - those with priority as number that can be compared or with a missing relative entry
261
// - those with a relative priority that must be sorted relative to another entry that exists
262
const mapEntryWithNumberedPriorityToIndex = new Map<IStatusbarViewModelEntry, number /* priority of entry as number */>();
263
const mapEntryWithRelativePriority = new Map<string /* id of entry to position after */, Map<string, IStatusbarViewModelEntry>>();
264
for (let i = 0; i < this._entries.length; i++) {
265
const entry = this._entries[i];
266
if (typeof entry.priority.primary === 'number' || !allEntryIds.has(entry.priority.primary.location.id)) {
267
mapEntryWithNumberedPriorityToIndex.set(entry, i);
268
} else {
269
const referenceEntryId = entry.priority.primary.location.id;
270
let entries = mapEntryWithRelativePriority.get(referenceEntryId);
271
if (!entries) {
272
273
// It is possible that this entry references another entry
274
// that itself references an entry. In that case, we want
275
// to add it to the entries of the referenced entry.
276
277
for (const relativeEntries of mapEntryWithRelativePriority.values()) {
278
if (relativeEntries.has(referenceEntryId)) {
279
entries = relativeEntries;
280
break;
281
}
282
}
283
284
if (!entries) {
285
entries = new Map();
286
mapEntryWithRelativePriority.set(referenceEntryId, entries);
287
}
288
}
289
entries.set(entry.id, entry);
290
}
291
}
292
293
// Sort the entries with `priority: number` or referencing a missing entry accordingly
294
const sortedEntriesWithNumberedPriority = Array.from(mapEntryWithNumberedPriorityToIndex.keys());
295
sortedEntriesWithNumberedPriority.sort((entryA, entryB) => {
296
if (entryA.alignment === entryB.alignment) {
297
298
// Sort by primary/secondary priority: higher values move towards the left
299
300
const entryAPrimaryPriority = typeof entryA.priority.primary === 'number' ? entryA.priority.primary : entryA.priority.primary.location.priority;
301
const entryBPrimaryPriority = typeof entryB.priority.primary === 'number' ? entryB.priority.primary : entryB.priority.primary.location.priority;
302
303
if (entryAPrimaryPriority !== entryBPrimaryPriority) {
304
return entryBPrimaryPriority - entryAPrimaryPriority;
305
}
306
307
if (entryA.priority.secondary !== entryB.priority.secondary) {
308
return entryB.priority.secondary - entryA.priority.secondary;
309
}
310
311
// otherwise maintain stable order (both values known to be in map)
312
return mapEntryWithNumberedPriorityToIndex.get(entryA)! - mapEntryWithNumberedPriorityToIndex.get(entryB)!;
313
}
314
315
if (entryA.alignment === StatusbarAlignment.LEFT) {
316
return -1;
317
}
318
319
if (entryB.alignment === StatusbarAlignment.LEFT) {
320
return 1;
321
}
322
323
return 0;
324
});
325
326
let sortedEntries: IStatusbarViewModelEntry[];
327
328
// Entries with location: sort in accordingly
329
if (mapEntryWithRelativePriority.size > 0) {
330
sortedEntries = [];
331
332
for (const entry of sortedEntriesWithNumberedPriority) {
333
const relativeEntriesMap = mapEntryWithRelativePriority.get(entry.id);
334
const relativeEntries = relativeEntriesMap ? Array.from(relativeEntriesMap.values()) : undefined;
335
336
// Fill relative entries to LEFT
337
if (relativeEntries) {
338
sortedEntries.push(...relativeEntries
339
.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.LEFT)
340
.sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));
341
}
342
343
// Fill referenced entry
344
sortedEntries.push(entry);
345
346
// Fill relative entries to RIGHT
347
if (relativeEntries) {
348
sortedEntries.push(...relativeEntries
349
.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.RIGHT)
350
.sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));
351
}
352
353
// Delete from map to mark as handled
354
mapEntryWithRelativePriority.delete(entry.id);
355
}
356
357
// Finally, just append all entries that reference another entry
358
// that does not exist to the end of the list
359
//
360
// Note: this should really not happen because of our check in
361
// `allEntryIds`, but we play it safe here to really consume
362
// all entries.
363
//
364
for (const [, entries] of mapEntryWithRelativePriority) {
365
sortedEntries.push(...Array.from(entries.values()).sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));
366
}
367
}
368
369
// No entries with relative priority: take sorted entries as is
370
else {
371
sortedEntries = sortedEntriesWithNumberedPriority;
372
}
373
374
// Take over as new truth of entries
375
this._entries = sortedEntries;
376
}
377
378
private markFirstLastVisibleEntry(): void {
379
this.doMarkFirstLastVisibleStatusbarItem(this.getEntries(StatusbarAlignment.LEFT));
380
this.doMarkFirstLastVisibleStatusbarItem(this.getEntries(StatusbarAlignment.RIGHT));
381
}
382
383
private doMarkFirstLastVisibleStatusbarItem(entries: IStatusbarViewModelEntry[]): void {
384
let firstVisibleItem: IStatusbarViewModelEntry | undefined;
385
let lastVisibleItem: IStatusbarViewModelEntry | undefined;
386
387
for (const entry of entries) {
388
389
// Clear previous first
390
entry.container.classList.remove('first-visible-item', 'last-visible-item');
391
392
const isVisible = !this.isHidden(entry.id);
393
if (isVisible) {
394
if (!firstVisibleItem) {
395
firstVisibleItem = entry;
396
}
397
398
lastVisibleItem = entry;
399
}
400
}
401
402
// Mark: first visible item
403
firstVisibleItem?.container.classList.add('first-visible-item');
404
405
// Mark: last visible item
406
lastVisibleItem?.container.classList.add('last-visible-item');
407
}
408
}
409
410