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
5310 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.delete(id)) {
148
this.updateVisibility(id, true);
149
150
this.saveState();
151
}
152
}
153
154
findEntry(container: HTMLElement): IStatusbarViewModelEntry | undefined {
155
return this._entries.find(entry => entry.container === container);
156
}
157
158
getEntries(alignment: StatusbarAlignment): IStatusbarViewModelEntry[] {
159
return this._entries.filter(entry => entry.alignment === alignment);
160
}
161
162
focusNextEntry(): void {
163
this.focusEntry(+1, 0);
164
}
165
166
focusPreviousEntry(): void {
167
this.focusEntry(-1, this.entries.length - 1);
168
}
169
170
isEntryFocused(): boolean {
171
return !!this.getFocusedEntry();
172
}
173
174
private getFocusedEntry(): IStatusbarViewModelEntry | undefined {
175
return this._entries.find(entry => isAncestorOfActiveElement(entry.container));
176
}
177
178
private focusEntry(delta: number, restartPosition: number): void {
179
180
const getVisibleEntry = (start: number) => {
181
let indexToFocus = start;
182
let entry = (indexToFocus >= 0 && indexToFocus < this._entries.length) ? this._entries[indexToFocus] : undefined;
183
while (entry && this.isHidden(entry.id)) {
184
indexToFocus += delta;
185
entry = (indexToFocus >= 0 && indexToFocus < this._entries.length) ? this._entries[indexToFocus] : undefined;
186
}
187
188
return entry;
189
};
190
191
const focused = this.getFocusedEntry();
192
if (focused) {
193
const entry = getVisibleEntry(this._entries.indexOf(focused) + delta);
194
if (entry) {
195
this._lastFocusedEntry = entry;
196
197
entry.labelContainer.focus();
198
199
return;
200
}
201
}
202
203
const entry = getVisibleEntry(restartPosition);
204
if (entry) {
205
this._lastFocusedEntry = entry;
206
entry.labelContainer.focus();
207
}
208
}
209
210
private updateVisibility(id: string, trigger: boolean): void;
211
private updateVisibility(entry: IStatusbarViewModelEntry, trigger: boolean): void;
212
private updateVisibility(arg1: string | IStatusbarViewModelEntry, trigger: boolean): void {
213
214
// By identifier
215
if (typeof arg1 === 'string') {
216
const id = arg1;
217
218
for (const entry of this._entries) {
219
if (entry.id === id) {
220
this.updateVisibility(entry, trigger);
221
}
222
}
223
}
224
225
// By entry
226
else {
227
const entry = arg1;
228
const isHidden = this.isHidden(entry.id);
229
230
// Use CSS to show/hide item container
231
if (isHidden) {
232
hide(entry.container);
233
} else {
234
show(entry.container);
235
}
236
237
if (trigger) {
238
this._onDidChangeEntryVisibility.fire({ id: entry.id, visible: !isHidden });
239
}
240
241
// Mark first/last visible entry
242
this.markFirstLastVisibleEntry();
243
}
244
}
245
246
private saveState(): void {
247
if (this.hidden.size > 0) {
248
this.storageService.store(StatusbarViewModel.HIDDEN_ENTRIES_KEY, JSON.stringify(Array.from(this.hidden.values())), StorageScope.PROFILE, StorageTarget.USER);
249
} else {
250
this.storageService.remove(StatusbarViewModel.HIDDEN_ENTRIES_KEY, StorageScope.PROFILE);
251
}
252
}
253
254
private sort(): void {
255
const allEntryIds = new Set(this._entries.map(entry => entry.id));
256
257
// Split up entries into 2 buckets:
258
// - those with priority as number that can be compared or with a missing relative entry
259
// - those with a relative priority that must be sorted relative to another entry that exists
260
const mapEntryWithNumberedPriorityToIndex = new Map<IStatusbarViewModelEntry, number /* priority of entry as number */>();
261
const mapEntryWithRelativePriority = new Map<string /* id of entry to position after */, Map<string, IStatusbarViewModelEntry>>();
262
for (let i = 0; i < this._entries.length; i++) {
263
const entry = this._entries[i];
264
if (typeof entry.priority.primary === 'number' || !allEntryIds.has(entry.priority.primary.location.id)) {
265
mapEntryWithNumberedPriorityToIndex.set(entry, i);
266
} else {
267
const referenceEntryId = entry.priority.primary.location.id;
268
let entries = mapEntryWithRelativePriority.get(referenceEntryId);
269
if (!entries) {
270
271
// It is possible that this entry references another entry
272
// that itself references an entry. In that case, we want
273
// to add it to the entries of the referenced entry.
274
275
for (const relativeEntries of mapEntryWithRelativePriority.values()) {
276
if (relativeEntries.has(referenceEntryId)) {
277
entries = relativeEntries;
278
break;
279
}
280
}
281
282
if (!entries) {
283
entries = new Map();
284
mapEntryWithRelativePriority.set(referenceEntryId, entries);
285
}
286
}
287
entries.set(entry.id, entry);
288
}
289
}
290
291
// Sort the entries with `priority: number` or referencing a missing entry accordingly
292
const sortedEntriesWithNumberedPriority = Array.from(mapEntryWithNumberedPriorityToIndex.keys());
293
sortedEntriesWithNumberedPriority.sort((entryA, entryB) => {
294
if (entryA.alignment === entryB.alignment) {
295
296
// Sort by primary/secondary priority: higher values move towards the left
297
298
const entryAPrimaryPriority = typeof entryA.priority.primary === 'number' ? entryA.priority.primary : entryA.priority.primary.location.priority;
299
const entryBPrimaryPriority = typeof entryB.priority.primary === 'number' ? entryB.priority.primary : entryB.priority.primary.location.priority;
300
301
if (entryAPrimaryPriority !== entryBPrimaryPriority) {
302
return entryBPrimaryPriority - entryAPrimaryPriority;
303
}
304
305
if (entryA.priority.secondary !== entryB.priority.secondary) {
306
return entryB.priority.secondary - entryA.priority.secondary;
307
}
308
309
// otherwise maintain stable order (both values known to be in map)
310
return mapEntryWithNumberedPriorityToIndex.get(entryA)! - mapEntryWithNumberedPriorityToIndex.get(entryB)!;
311
}
312
313
if (entryA.alignment === StatusbarAlignment.LEFT) {
314
return -1;
315
}
316
317
if (entryB.alignment === StatusbarAlignment.LEFT) {
318
return 1;
319
}
320
321
return 0;
322
});
323
324
let sortedEntries: IStatusbarViewModelEntry[];
325
326
// Entries with location: sort in accordingly
327
if (mapEntryWithRelativePriority.size > 0) {
328
sortedEntries = [];
329
330
for (const entry of sortedEntriesWithNumberedPriority) {
331
const relativeEntriesMap = mapEntryWithRelativePriority.get(entry.id);
332
const relativeEntries = relativeEntriesMap ? Array.from(relativeEntriesMap.values()) : undefined;
333
334
// Fill relative entries to LEFT
335
if (relativeEntries) {
336
sortedEntries.push(...relativeEntries
337
.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.LEFT)
338
.sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));
339
}
340
341
// Fill referenced entry
342
sortedEntries.push(entry);
343
344
// Fill relative entries to RIGHT
345
if (relativeEntries) {
346
sortedEntries.push(...relativeEntries
347
.filter(entry => isStatusbarEntryLocation(entry.priority.primary) && entry.priority.primary.alignment === StatusbarAlignment.RIGHT)
348
.sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));
349
}
350
351
// Delete from map to mark as handled
352
mapEntryWithRelativePriority.delete(entry.id);
353
}
354
355
// Finally, just append all entries that reference another entry
356
// that does not exist to the end of the list
357
//
358
// Note: this should really not happen because of our check in
359
// `allEntryIds`, but we play it safe here to really consume
360
// all entries.
361
//
362
for (const [, entries] of mapEntryWithRelativePriority) {
363
sortedEntries.push(...Array.from(entries.values()).sort((entryA, entryB) => entryB.priority.secondary - entryA.priority.secondary));
364
}
365
}
366
367
// No entries with relative priority: take sorted entries as is
368
else {
369
sortedEntries = sortedEntriesWithNumberedPriority;
370
}
371
372
// Take over as new truth of entries
373
this._entries = sortedEntries;
374
}
375
376
private markFirstLastVisibleEntry(): void {
377
this.doMarkFirstLastVisibleStatusbarItem(this.getEntries(StatusbarAlignment.LEFT));
378
this.doMarkFirstLastVisibleStatusbarItem(this.getEntries(StatusbarAlignment.RIGHT));
379
}
380
381
private doMarkFirstLastVisibleStatusbarItem(entries: IStatusbarViewModelEntry[]): void {
382
let firstVisibleItem: IStatusbarViewModelEntry | undefined;
383
let lastVisibleItem: IStatusbarViewModelEntry | undefined;
384
385
for (const entry of entries) {
386
387
// Clear previous first
388
entry.container.classList.remove('first-visible-item', 'last-visible-item');
389
390
const isVisible = !this.isHidden(entry.id);
391
if (isVisible) {
392
if (!firstVisibleItem) {
393
firstVisibleItem = entry;
394
}
395
396
lastVisibleItem = entry;
397
}
398
}
399
400
// Mark: first visible item
401
firstVisibleItem?.container.classList.add('first-visible-item');
402
403
// Mark: last visible item
404
lastVisibleItem?.container.classList.add('last-visible-item');
405
}
406
}
407
408