Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts
5221 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 './media/extensionsWidgets.css';
7
import * as semver from '../../../../base/common/semver/semver.js';
8
import { Disposable, toDisposable, DisposableStore, MutableDisposable, IDisposable } from '../../../../base/common/lifecycle.js';
9
import { IExtension, IExtensionsWorkbenchService, IExtensionContainer, ExtensionState, ExtensionEditorTab, IExtensionsViewState } from '../common/extensions.js';
10
import { append, $, reset, addDisposableListener, EventType, finalHandler } from '../../../../base/browser/dom.js';
11
import * as platform from '../../../../base/common/platform.js';
12
import { localize } from '../../../../nls.js';
13
import { IExtensionManagementServerService } from '../../../services/extensionManagement/common/extensionManagement.js';
14
import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';
15
import { ILabelService } from '../../../../platform/label/common/label.js';
16
import { extensionButtonProminentBackground, ExtensionStatusAction } from './extensionsActions.js';
17
import { IThemeService, registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
18
import { ThemeIcon } from '../../../../base/common/themables.js';
19
import { EXTENSION_BADGE_BACKGROUND, EXTENSION_BADGE_FOREGROUND } from '../../../common/theme.js';
20
import { Emitter, Event } from '../../../../base/common/event.js';
21
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
22
import { CountBadge } from '../../../../base/browser/ui/countBadge/countBadge.js';
23
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
24
import { IUserDataSyncEnablementService } from '../../../../platform/userDataSync/common/userDataSync.js';
25
import { activationTimeIcon, errorIcon, infoIcon, installCountIcon, preReleaseIcon, privateExtensionIcon, ratingIcon, remoteIcon, sponsorIcon, starEmptyIcon, starFullIcon, starHalfIcon, syncIgnoredIcon, warningIcon } from './extensionsIcons.js';
26
import { registerColor, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js';
27
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
28
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
29
import { createCommandUri, MarkdownString } from '../../../../base/common/htmlContent.js';
30
import { URI } from '../../../../base/common/uri.js';
31
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
32
import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
33
import Severity from '../../../../base/common/severity.js';
34
import { Color } from '../../../../base/common/color.js';
35
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
36
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
37
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
38
import { KeyCode } from '../../../../base/common/keyCodes.js';
39
import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js';
40
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
41
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
42
import type { IManagedHover } from '../../../../base/browser/ui/hover/hover.js';
43
import { Registry } from '../../../../platform/registry/common/platform.js';
44
import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js';
45
import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js';
46
import { extensionDefaultIcon, extensionVerifiedPublisherIconColor, verifiedPublisherIcon } from '../../../services/extensionManagement/common/extensionsIcons.js';
47
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
48
import { IExplorerService } from '../../files/browser/files.js';
49
import { IViewsService } from '../../../services/views/common/viewsService.js';
50
import { VIEW_ID as EXPLORER_VIEW_ID } from '../../files/common/files.js';
51
import { IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
52
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
53
54
export abstract class ExtensionWidget extends Disposable implements IExtensionContainer {
55
private _extension: IExtension | null = null;
56
get extension(): IExtension | null { return this._extension; }
57
set extension(extension: IExtension | null) { this._extension = extension; this.update(); }
58
update(): void { this.render(); }
59
abstract render(): void;
60
}
61
62
export function onClick(element: HTMLElement, callback: () => void): IDisposable {
63
const disposables: DisposableStore = new DisposableStore();
64
disposables.add(addDisposableListener(element, EventType.CLICK, finalHandler(callback)));
65
disposables.add(addDisposableListener(element, EventType.KEY_UP, e => {
66
const keyboardEvent = new StandardKeyboardEvent(e);
67
if (keyboardEvent.equals(KeyCode.Space) || keyboardEvent.equals(KeyCode.Enter)) {
68
e.preventDefault();
69
e.stopPropagation();
70
callback();
71
}
72
}));
73
return disposables;
74
}
75
76
export class ExtensionIconWidget extends ExtensionWidget {
77
78
private readonly iconLoadingDisposable = this._register(new MutableDisposable());
79
private readonly iconErrorDisposable = this._register(new MutableDisposable());
80
private readonly element: HTMLElement;
81
private readonly iconElement: HTMLImageElement;
82
private readonly defaultIconElement: HTMLElement;
83
84
private iconUrl: string | undefined;
85
86
constructor(
87
container: HTMLElement,
88
) {
89
super();
90
this.element = append(container, $('.extension-icon'));
91
92
this.iconElement = append(this.element, $('img.icon', { alt: '' }));
93
this.iconElement.style.display = 'none';
94
95
this.defaultIconElement = append(this.element, $(ThemeIcon.asCSSSelector(extensionDefaultIcon)));
96
this.defaultIconElement.style.display = 'none';
97
98
this.render();
99
this._register(toDisposable(() => this.clear()));
100
}
101
102
private clear(): void {
103
this.iconUrl = undefined;
104
this.iconElement.src = '';
105
this.iconElement.style.display = 'none';
106
this.defaultIconElement.style.display = 'none';
107
this.iconErrorDisposable.clear();
108
this.iconLoadingDisposable.clear();
109
}
110
111
render(): void {
112
if (!this.extension) {
113
this.clear();
114
return;
115
}
116
117
if (this.extension.iconUrl) {
118
if (this.iconUrl !== this.extension.iconUrl) {
119
this.iconElement.style.display = 'inherit';
120
this.defaultIconElement.style.display = 'none';
121
this.iconUrl = this.extension.iconUrl;
122
this.iconErrorDisposable.value = addDisposableListener(this.iconElement, 'error', () => {
123
if (this.extension?.iconUrlFallback) {
124
this.iconElement.src = this.extension.iconUrlFallback;
125
} else {
126
this.iconElement.style.display = 'none';
127
this.defaultIconElement.style.display = 'inherit';
128
}
129
}, { once: true });
130
this.iconElement.src = this.iconUrl;
131
if (!this.iconElement.complete) {
132
this.iconElement.style.visibility = 'hidden';
133
this.iconLoadingDisposable.value = addDisposableListener(this.iconElement, 'load', () => {
134
this.iconElement.style.visibility = 'inherit';
135
});
136
} else {
137
this.iconElement.style.visibility = 'inherit';
138
}
139
}
140
} else {
141
this.iconUrl = undefined;
142
this.iconElement.style.display = 'none';
143
this.iconElement.src = '';
144
this.defaultIconElement.style.display = 'inherit';
145
this.iconErrorDisposable.clear();
146
this.iconLoadingDisposable.clear();
147
}
148
}
149
}
150
151
export class InstallCountWidget extends ExtensionWidget {
152
153
private readonly disposables = this._register(new DisposableStore());
154
155
constructor(
156
readonly container: HTMLElement,
157
private small: boolean,
158
@IHoverService private readonly hoverService: IHoverService,
159
) {
160
super();
161
this.render();
162
163
this._register(toDisposable(() => this.clear()));
164
}
165
166
private clear(): void {
167
this.container.innerText = '';
168
this.disposables.clear();
169
}
170
171
render(): void {
172
this.clear();
173
174
if (!this.extension) {
175
return;
176
}
177
178
if (this.small && this.extension.state !== ExtensionState.Uninstalled) {
179
return;
180
}
181
182
const installLabel = InstallCountWidget.getInstallLabel(this.extension, this.small);
183
if (!installLabel) {
184
return;
185
}
186
187
const parent = this.small ? this.container : append(this.container, $('span.install', { tabIndex: 0 }));
188
append(parent, $('span' + ThemeIcon.asCSSSelector(installCountIcon)));
189
const count = append(parent, $('span.count'));
190
count.textContent = installLabel;
191
192
if (!this.small) {
193
this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.container, localize('install count', "Install count")));
194
}
195
}
196
197
static getInstallLabel(extension: IExtension, small: boolean): string | undefined {
198
const installCount = extension.installCount;
199
200
if (!installCount) {
201
return undefined;
202
}
203
204
let installLabel: string;
205
206
if (small) {
207
if (installCount > 1000000) {
208
installLabel = `${Math.floor(installCount / 100000) / 10}M`;
209
} else if (installCount > 1000) {
210
installLabel = `${Math.floor(installCount / 1000)}K`;
211
} else {
212
installLabel = String(installCount);
213
}
214
}
215
else {
216
installLabel = installCount.toLocaleString(platform.language);
217
}
218
219
return installLabel;
220
}
221
}
222
223
export class RatingsWidget extends ExtensionWidget {
224
225
private containerHover: IManagedHover | undefined;
226
private readonly disposables = this._register(new DisposableStore());
227
228
constructor(
229
readonly container: HTMLElement,
230
private small: boolean,
231
@IHoverService private readonly hoverService: IHoverService,
232
@IOpenerService private readonly openerService: IOpenerService,
233
) {
234
super();
235
container.classList.add('extension-ratings');
236
237
if (this.small) {
238
container.classList.add('small');
239
}
240
241
this.render();
242
this._register(toDisposable(() => this.clear()));
243
}
244
245
private clear(): void {
246
this.container.innerText = '';
247
this.disposables.clear();
248
}
249
250
render(): void {
251
this.clear();
252
253
if (!this.extension) {
254
return;
255
}
256
257
if (this.small && this.extension.state !== ExtensionState.Uninstalled) {
258
return;
259
}
260
261
if (this.extension.rating === undefined) {
262
return;
263
}
264
265
if (this.small && !this.extension.ratingCount) {
266
return;
267
}
268
269
if (!this.extension.url) {
270
return;
271
}
272
273
const rating = Math.round(this.extension.rating * 2) / 2;
274
if (this.small) {
275
append(this.container, $('span' + ThemeIcon.asCSSSelector(starFullIcon)));
276
277
const count = append(this.container, $('span.count'));
278
count.textContent = String(rating);
279
} else {
280
const element = append(this.container, $('span.rating.clickable', { tabIndex: 0 }));
281
for (let i = 1; i <= 5; i++) {
282
if (rating >= i) {
283
append(element, $('span' + ThemeIcon.asCSSSelector(starFullIcon)));
284
} else if (rating >= i - 0.5) {
285
append(element, $('span' + ThemeIcon.asCSSSelector(starHalfIcon)));
286
} else {
287
append(element, $('span' + ThemeIcon.asCSSSelector(starEmptyIcon)));
288
}
289
}
290
if (this.extension.ratingCount) {
291
const ratingCountElemet = append(element, $('span', undefined, ` (${this.extension.ratingCount})`));
292
ratingCountElemet.style.paddingLeft = '1px';
293
}
294
295
this.containerHover = this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, ''));
296
this.containerHover.update(localize('ratedLabel', "Average rating: {0} out of 5", rating));
297
element.setAttribute('role', 'link');
298
if (this.extension.ratingUrl) {
299
this.disposables.add(onClick(element, () => this.openerService.open(URI.parse(this.extension!.ratingUrl!))));
300
}
301
}
302
}
303
304
}
305
306
export class PublisherWidget extends ExtensionWidget {
307
308
private element: HTMLElement | undefined;
309
private containerHover: IManagedHover | undefined;
310
311
private readonly disposables = this._register(new DisposableStore());
312
313
constructor(
314
readonly container: HTMLElement,
315
private small: boolean,
316
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
317
@IHoverService private readonly hoverService: IHoverService,
318
@IOpenerService private readonly openerService: IOpenerService,
319
) {
320
super();
321
322
this.render();
323
this._register(toDisposable(() => this.clear()));
324
}
325
326
private clear(): void {
327
this.element?.remove();
328
this.disposables.clear();
329
}
330
331
render(): void {
332
this.clear();
333
if (!this.extension) {
334
return;
335
}
336
337
if (this.extension.resourceExtension) {
338
return;
339
}
340
341
if (this.extension.local?.source === 'resource') {
342
return;
343
}
344
345
this.element = append(this.container, $('.publisher'));
346
const publisherDisplayName = $('.publisher-name.ellipsis');
347
publisherDisplayName.textContent = this.extension.publisherDisplayName;
348
349
const verifiedPublisher = $('.verified-publisher');
350
append(verifiedPublisher, $('span.extension-verified-publisher.clickable'), renderIcon(verifiedPublisherIcon));
351
352
if (this.small) {
353
if (this.extension.publisherDomain?.verified) {
354
append(this.element, verifiedPublisher);
355
}
356
append(this.element, publisherDisplayName);
357
} else {
358
this.element.classList.toggle('clickable', !!this.extension.url);
359
this.element.setAttribute('role', 'button');
360
this.element.tabIndex = 0;
361
362
this.containerHover = this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, localize('publisher', "Publisher ({0})", this.extension.publisherDisplayName)));
363
append(this.element, publisherDisplayName);
364
365
if (this.extension.publisherDomain?.verified) {
366
append(this.element, verifiedPublisher);
367
const publisherDomainLink = URI.parse(this.extension.publisherDomain.link);
368
verifiedPublisher.tabIndex = 0;
369
verifiedPublisher.setAttribute('role', 'button');
370
this.containerHover.update(localize('verified publisher', "This publisher has verified ownership of {0}", this.extension.publisherDomain.link));
371
verifiedPublisher.setAttribute('role', 'link');
372
373
append(verifiedPublisher, $('span.extension-verified-publisher-domain', undefined, publisherDomainLink.authority.startsWith('www.') ? publisherDomainLink.authority.substring(4) : publisherDomainLink.authority));
374
this.disposables.add(onClick(verifiedPublisher, () => this.openerService.open(publisherDomainLink)));
375
}
376
377
if (this.extension.url) {
378
this.disposables.add(onClick(this.element, () => this.extensionsWorkbenchService.openSearch(`publisher:"${this.extension?.publisherDisplayName}"`)));
379
}
380
}
381
382
}
383
384
}
385
386
export class SponsorWidget extends ExtensionWidget {
387
388
private readonly disposables = this._register(new DisposableStore());
389
390
constructor(
391
readonly container: HTMLElement,
392
@IHoverService private readonly hoverService: IHoverService,
393
@IOpenerService private readonly openerService: IOpenerService,
394
) {
395
super();
396
this.render();
397
}
398
399
render(): void {
400
reset(this.container);
401
this.disposables.clear();
402
if (!this.extension?.publisherSponsorLink) {
403
return;
404
}
405
406
const sponsor = append(this.container, $('span.sponsor.clickable', { tabIndex: 0 }));
407
this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), sponsor, this.extension?.publisherSponsorLink.toString() ?? ''));
408
sponsor.setAttribute('role', 'link'); // #132645
409
const sponsorIconElement = renderIcon(sponsorIcon);
410
const label = $('span', undefined, localize('sponsor', "Sponsor"));
411
append(sponsor, sponsorIconElement, label);
412
this.disposables.add(onClick(sponsor, () => {
413
this.openerService.open(this.extension!.publisherSponsorLink!);
414
}));
415
}
416
}
417
418
export class RecommendationWidget extends ExtensionWidget {
419
420
private element?: HTMLElement;
421
private readonly disposables = this._register(new DisposableStore());
422
423
constructor(
424
private parent: HTMLElement,
425
@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService
426
) {
427
super();
428
this.render();
429
this._register(toDisposable(() => this.clear()));
430
this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => this.render()));
431
}
432
433
private clear(): void {
434
this.element?.remove();
435
this.element = undefined;
436
this.disposables.clear();
437
}
438
439
render(): void {
440
this.clear();
441
if (!this.extension || this.extension.state === ExtensionState.Installed || this.extension.deprecationInfo) {
442
return;
443
}
444
const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();
445
if (extRecommendations[this.extension.identifier.id.toLowerCase()]) {
446
this.element = append(this.parent, $('div.extension-bookmark'));
447
const recommendation = append(this.element, $('.recommendation'));
448
append(recommendation, $('span' + ThemeIcon.asCSSSelector(ratingIcon)));
449
}
450
}
451
452
}
453
454
export class PreReleaseBookmarkWidget extends ExtensionWidget {
455
456
private element?: HTMLElement;
457
private readonly disposables = this._register(new DisposableStore());
458
459
constructor(
460
private parent: HTMLElement,
461
) {
462
super();
463
this.render();
464
this._register(toDisposable(() => this.clear()));
465
}
466
467
private clear(): void {
468
this.element?.remove();
469
this.element = undefined;
470
this.disposables.clear();
471
}
472
473
render(): void {
474
this.clear();
475
if (this.extension?.state === ExtensionState.Installed ? this.extension.preRelease : this.extension?.hasPreReleaseVersion) {
476
this.element = append(this.parent, $('div.extension-bookmark'));
477
const preRelease = append(this.element, $('.pre-release'));
478
append(preRelease, $('span' + ThemeIcon.asCSSSelector(preReleaseIcon)));
479
}
480
}
481
482
}
483
484
export class RemoteBadgeWidget extends ExtensionWidget {
485
486
private readonly remoteBadge = this._register(new MutableDisposable<ExtensionIconBadge>());
487
488
private element: HTMLElement;
489
490
constructor(
491
parent: HTMLElement,
492
private readonly tooltip: boolean,
493
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
494
@IInstantiationService private readonly instantiationService: IInstantiationService
495
) {
496
super();
497
this.element = append(parent, $(''));
498
this.render();
499
this._register(toDisposable(() => this.clear()));
500
}
501
502
private clear(): void {
503
this.remoteBadge.value?.element.remove();
504
this.remoteBadge.clear();
505
}
506
507
render(): void {
508
this.clear();
509
if (!this.extension || !this.extension.local || !this.extension.server || !(this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) || this.extension.server !== this.extensionManagementServerService.remoteExtensionManagementServer) {
510
return;
511
}
512
let tooltip: string | undefined;
513
if (this.tooltip && this.extensionManagementServerService.remoteExtensionManagementServer) {
514
tooltip = localize('remote extension title', "Extension in {0}", this.extensionManagementServerService.remoteExtensionManagementServer.label);
515
}
516
this.remoteBadge.value = this.instantiationService.createInstance(ExtensionIconBadge, remoteIcon, tooltip);
517
append(this.element, this.remoteBadge.value.element);
518
}
519
}
520
521
export class ExtensionIconBadge extends Disposable {
522
523
readonly element: HTMLElement;
524
readonly elementHover: IManagedHover;
525
526
constructor(
527
private readonly icon: ThemeIcon,
528
private readonly tooltip: string | undefined,
529
@IHoverService hoverService: IHoverService,
530
@ILabelService private readonly labelService: ILabelService,
531
@IThemeService private readonly themeService: IThemeService,
532
) {
533
super();
534
this.element = $('div.extension-badge.extension-icon-badge');
535
this.elementHover = this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, ''));
536
this.render();
537
}
538
539
private render(): void {
540
append(this.element, $('span' + ThemeIcon.asCSSSelector(this.icon)));
541
542
const applyBadgeStyle = () => {
543
if (!this.element) {
544
return;
545
}
546
const bgColor = this.themeService.getColorTheme().getColor(EXTENSION_BADGE_BACKGROUND);
547
const fgColor = this.themeService.getColorTheme().getColor(EXTENSION_BADGE_FOREGROUND);
548
this.element.style.backgroundColor = bgColor ? bgColor.toString() : '';
549
this.element.style.color = fgColor ? fgColor.toString() : '';
550
};
551
applyBadgeStyle();
552
this._register(this.themeService.onDidColorThemeChange(() => applyBadgeStyle()));
553
554
if (this.tooltip) {
555
const updateTitle = () => {
556
if (this.element) {
557
this.elementHover.update(this.tooltip);
558
}
559
};
560
this._register(this.labelService.onDidChangeFormatters(() => updateTitle()));
561
updateTitle();
562
}
563
}
564
}
565
566
export class ExtensionPackCountWidget extends ExtensionWidget {
567
568
private element: HTMLElement | undefined;
569
private countBadge: CountBadge | undefined;
570
571
constructor(
572
private readonly parent: HTMLElement,
573
) {
574
super();
575
this.render();
576
this._register(toDisposable(() => this.clear()));
577
}
578
579
private clear(): void {
580
this.element?.remove();
581
this.countBadge?.dispose();
582
this.countBadge = undefined;
583
}
584
585
render(): void {
586
this.clear();
587
if (!this.extension || !(this.extension.categories?.some(category => category.toLowerCase() === 'extension packs')) || !this.extension.extensionPack.length) {
588
return;
589
}
590
this.element = append(this.parent, $('.extension-badge.extension-pack-badge'));
591
this.countBadge = new CountBadge(this.element, {}, defaultCountBadgeStyles);
592
this.countBadge.setCount(this.extension.extensionPack.length);
593
}
594
}
595
596
export class ExtensionKindIndicatorWidget extends ExtensionWidget {
597
598
private element: HTMLElement | undefined;
599
private extensionGalleryManifest: IExtensionGalleryManifest | null = null;
600
601
private readonly disposables = this._register(new DisposableStore());
602
603
constructor(
604
readonly container: HTMLElement,
605
private small: boolean,
606
@IHoverService private readonly hoverService: IHoverService,
607
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
608
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
609
@IExplorerService private readonly explorerService: IExplorerService,
610
@IViewsService private readonly viewsService: IViewsService,
611
@IExtensionGalleryManifestService extensionGalleryManifestService: IExtensionGalleryManifestService,
612
) {
613
super();
614
this.render();
615
this._register(toDisposable(() => this.clear()));
616
extensionGalleryManifestService.getExtensionGalleryManifest().then(manifest => {
617
if (this._store.isDisposed) {
618
return;
619
}
620
this.extensionGalleryManifest = manifest;
621
this.render();
622
});
623
}
624
625
private clear(): void {
626
this.element?.remove();
627
this.disposables.clear();
628
}
629
630
render(): void {
631
this.clear();
632
633
if (!this.extension) {
634
return;
635
}
636
637
if (this.extension?.private) {
638
this.element = append(this.container, $('.extension-kind-indicator'));
639
if (!this.small || (this.extensionGalleryManifest?.capabilities.extensions?.includePublicExtensions && this.extensionGalleryManifest?.capabilities.extensions?.includePrivateExtensions)) {
640
append(this.element, $('span' + ThemeIcon.asCSSSelector(privateExtensionIcon)));
641
}
642
if (!this.small) {
643
append(this.element, $('span.private-extension-label', undefined, localize('privateExtension', "Private Extension")));
644
}
645
return;
646
}
647
648
if (!this.small) {
649
return;
650
}
651
652
const location = this.extension.resourceExtension?.location ?? (this.extension.local?.source === 'resource' ? this.extension.local?.location : undefined);
653
if (!location) {
654
return;
655
}
656
657
this.element = append(this.container, $('.extension-kind-indicator'));
658
const workspaceFolder = this.contextService.getWorkspaceFolder(location);
659
if (workspaceFolder && this.extension.isWorkspaceScoped) {
660
this.element.textContent = localize('workspace extension', "Workspace Extension");
661
this.element.classList.add('clickable');
662
this.element.setAttribute('role', 'button');
663
this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, this.uriIdentityService.extUri.relativePath(workspaceFolder.uri, location)));
664
this.disposables.add(onClick(this.element, () => {
665
this.viewsService.openView(EXPLORER_VIEW_ID, true).then(() => this.explorerService.select(location, true));
666
}));
667
} else {
668
this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, location.path));
669
this.element.textContent = localize('local extension', "Local Extension");
670
}
671
}
672
}
673
674
export class SyncIgnoredWidget extends ExtensionWidget {
675
676
private readonly disposables = this._register(new DisposableStore());
677
678
constructor(
679
private readonly container: HTMLElement,
680
@IConfigurationService private readonly configurationService: IConfigurationService,
681
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
682
@IHoverService private readonly hoverService: IHoverService,
683
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
684
) {
685
super();
686
this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('settingsSync.ignoredExtensions'))(() => this.render()));
687
this._register(userDataSyncEnablementService.onDidChangeEnablement(() => this.update()));
688
this.render();
689
}
690
691
render(): void {
692
this.disposables.clear();
693
this.container.innerText = '';
694
695
if (this.extension && this.extension.state === ExtensionState.Installed && this.userDataSyncEnablementService.isEnabled() && this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension)) {
696
const element = append(this.container, $('span.extension-sync-ignored' + ThemeIcon.asCSSSelector(syncIgnoredIcon)));
697
this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element, localize('syncingore.label', "This extension is ignored during sync.")));
698
element.classList.add(...ThemeIcon.asClassNameArray(syncIgnoredIcon));
699
}
700
}
701
}
702
703
export class ExtensionRuntimeStatusWidget extends ExtensionWidget {
704
705
constructor(
706
private readonly extensionViewState: IExtensionsViewState,
707
private readonly container: HTMLElement,
708
@IExtensionService extensionService: IExtensionService,
709
@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,
710
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
711
) {
712
super();
713
this._register(extensionService.onDidChangeExtensionsStatus(extensions => {
714
if (this.extension && extensions.some(e => areSameExtensions({ id: e.value }, this.extension!.identifier))) {
715
this.update();
716
}
717
}));
718
this._register(extensionFeaturesManagementService.onDidChangeAccessData(e => {
719
if (this.extension && ExtensionIdentifier.equals(this.extension.identifier.id, e.extension)) {
720
this.update();
721
}
722
}));
723
}
724
725
render(): void {
726
this.container.innerText = '';
727
728
if (!this.extension) {
729
return;
730
}
731
732
if (this.extensionViewState.filters.featureId && this.extension.state === ExtensionState.Installed) {
733
const accessData = this.extensionFeaturesManagementService.getAllAccessDataForExtension(new ExtensionIdentifier(this.extension.identifier.id)).get(this.extensionViewState.filters.featureId);
734
const feature = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry).getExtensionFeature(this.extensionViewState.filters.featureId);
735
if (feature?.icon && accessData) {
736
const featureAccessTimeElement = append(this.container, $('span.activationTime'));
737
featureAccessTimeElement.textContent = localize('feature access label', "{0} reqs", accessData.accessTimes.length);
738
const iconElement = append(this.container, $('span' + ThemeIcon.asCSSSelector(feature.icon)));
739
iconElement.style.paddingLeft = '4px';
740
return;
741
}
742
}
743
744
const extensionStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension);
745
if (extensionStatus?.activationTimes) {
746
const activationTime = extensionStatus.activationTimes.codeLoadingTime + extensionStatus.activationTimes.activateCallTime;
747
append(this.container, $('span' + ThemeIcon.asCSSSelector(activationTimeIcon)));
748
const activationTimeElement = append(this.container, $('span.activationTime'));
749
activationTimeElement.textContent = `${activationTime}ms`;
750
}
751
}
752
753
}
754
755
export type ExtensionHoverOptions = {
756
position: () => HoverPosition;
757
readonly target: HTMLElement;
758
};
759
760
export class ExtensionHoverWidget extends ExtensionWidget {
761
762
private readonly hover = this._register(new MutableDisposable<IDisposable>());
763
764
constructor(
765
private readonly options: ExtensionHoverOptions,
766
private readonly extensionStatusAction: ExtensionStatusAction,
767
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
768
@IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService,
769
@IHoverService private readonly hoverService: IHoverService,
770
@IConfigurationService private readonly configurationService: IConfigurationService,
771
@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService,
772
@IThemeService private readonly themeService: IThemeService,
773
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
774
) {
775
super();
776
}
777
778
render(): void {
779
this.hover.value = undefined;
780
if (this.extension) {
781
this.hover.value = this.hoverService.setupManagedHover({
782
delay: this.configurationService.getValue<number>('workbench.hover.delay'),
783
showHover: (options, focus) => {
784
return this.hoverService.showInstantHover({
785
...options,
786
additionalClasses: ['extension-hover'],
787
position: {
788
hoverPosition: this.options.position(),
789
forcePosition: true,
790
},
791
persistence: {
792
hideOnKeyDown: true,
793
}
794
}, focus);
795
},
796
placement: 'element'
797
},
798
this.options.target,
799
{
800
markdown: () => Promise.resolve(this.getHoverMarkdown()),
801
markdownNotSupportedFallback: undefined
802
},
803
{
804
appearance: {
805
showHoverHint: true
806
}
807
}
808
);
809
}
810
}
811
812
private getHoverMarkdown(): MarkdownString | undefined {
813
if (!this.extension) {
814
return undefined;
815
}
816
const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });
817
818
markdown.appendMarkdown(`**${this.extension.displayName}**`);
819
if (semver.valid(this.extension.version)) {
820
markdown.appendMarkdown(`&nbsp;<span style="background-color:#8080802B;">**&nbsp;_v${this.extension.version}${(this.extension.isPreReleaseVersion ? ' (pre-release)' : '')}_**&nbsp;</span>`);
821
}
822
markdown.appendText(`\n`);
823
824
let addSeparator = false;
825
if (this.extension.private) {
826
markdown.appendMarkdown(`$(${privateExtensionIcon.id}) ${localize('privateExtension', "Private Extension")}`);
827
addSeparator = true;
828
}
829
if (this.extension.state === ExtensionState.Installed) {
830
const installLabel = InstallCountWidget.getInstallLabel(this.extension, true);
831
if (installLabel) {
832
if (addSeparator) {
833
markdown.appendText(` | `);
834
}
835
markdown.appendMarkdown(`$(${installCountIcon.id}) ${installLabel}`);
836
addSeparator = true;
837
}
838
if (this.extension.rating) {
839
if (addSeparator) {
840
markdown.appendText(` | `);
841
}
842
const rating = Math.round(this.extension.rating * 2) / 2;
843
markdown.appendMarkdown(`$(${starFullIcon.id}) [${rating}](${this.extension.url}&ssr=false#review-details)`);
844
addSeparator = true;
845
}
846
if (this.extension.publisherSponsorLink) {
847
if (addSeparator) {
848
markdown.appendText(` | `);
849
}
850
markdown.appendMarkdown(`$(${sponsorIcon.id}) [${localize('sponsor', "Sponsor")}](${this.extension.publisherSponsorLink})`);
851
addSeparator = true;
852
}
853
}
854
if (addSeparator) {
855
markdown.appendText(`\n`);
856
}
857
858
const location = this.extension.resourceExtension?.location ?? (this.extension.local?.source === 'resource' ? this.extension.local?.location : undefined);
859
if (location) {
860
if (this.extension.isWorkspaceScoped && this.contextService.isInsideWorkspace(location)) {
861
markdown.appendMarkdown(localize('workspace extension', "Workspace Extension"));
862
} else {
863
markdown.appendMarkdown(localize('local extension', "Local Extension"));
864
}
865
markdown.appendText(`\n`);
866
}
867
868
if (this.extension.description) {
869
markdown.appendMarkdown(`${this.extension.description}`);
870
markdown.appendText(`\n`);
871
}
872
873
if (this.extension.publisherDomain?.verified) {
874
const bgColor = this.themeService.getColorTheme().getColor(extensionVerifiedPublisherIconColor);
875
const publisherVerifiedTooltip = localize('publisher verified tooltip', "This publisher has verified ownership of {0}", `[${URI.parse(this.extension.publisherDomain.link).authority}](${this.extension.publisherDomain.link})`);
876
markdown.appendMarkdown(`<span style="color:${bgColor ? Color.Format.CSS.formatHex(bgColor) : '#ffffff'};">$(${verifiedPublisherIcon.id})</span>&nbsp;${publisherVerifiedTooltip}`);
877
markdown.appendText(`\n`);
878
}
879
880
if (this.extension.outdated) {
881
markdown.appendMarkdown(localize('updateRequired', "Latest version:"));
882
markdown.appendMarkdown(`&nbsp;<span style="background-color:#8080802B;">**&nbsp;_v${this.extension.latestVersion}_**&nbsp;</span>`);
883
markdown.appendText(`\n`);
884
}
885
886
const preReleaseMessage = ExtensionHoverWidget.getPreReleaseMessage(this.extension);
887
const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension);
888
const extensionFeaturesAccessData = this.extensionFeaturesManagementService.getAllAccessDataForExtension(new ExtensionIdentifier(this.extension.identifier.id));
889
const extensionStatus = this.extensionStatusAction.status;
890
const runtimeState = this.extension.runtimeState;
891
const recommendationMessage = this.getRecommendationMessage(this.extension);
892
893
if (extensionRuntimeStatus || extensionFeaturesAccessData.size || extensionStatus.length || runtimeState || recommendationMessage || preReleaseMessage) {
894
895
markdown.appendMarkdown(`---`);
896
markdown.appendText(`\n`);
897
898
if (extensionRuntimeStatus) {
899
if (extensionRuntimeStatus.activationTimes) {
900
const activationTime = extensionRuntimeStatus.activationTimes.codeLoadingTime + extensionRuntimeStatus.activationTimes.activateCallTime;
901
markdown.appendMarkdown(`${localize('activation', "Activation time")}${extensionRuntimeStatus.activationTimes.activationReason.startup ? ` (${localize('startup', "Startup")})` : ''}: \`${activationTime}ms\``);
902
markdown.appendText(`\n`);
903
}
904
if (extensionRuntimeStatus.runtimeErrors.length || extensionRuntimeStatus.messages.length) {
905
const hasErrors = extensionRuntimeStatus.runtimeErrors.length || extensionRuntimeStatus.messages.some(message => message.type === Severity.Error);
906
const hasWarnings = extensionRuntimeStatus.messages.some(message => message.type === Severity.Warning);
907
const errorsLink = extensionRuntimeStatus.runtimeErrors.length ? `[${extensionRuntimeStatus.runtimeErrors.length === 1 ? localize('uncaught error', '1 uncaught error') : localize('uncaught errors', '{0} uncaught errors', extensionRuntimeStatus.runtimeErrors.length)}](${createCommandUri('extension.open', this.extension.identifier.id, ExtensionEditorTab.Features)})` : undefined;
908
const messageLink = extensionRuntimeStatus.messages.length ? `[${extensionRuntimeStatus.messages.length === 1 ? localize('message', '1 message') : localize('messages', '{0} messages', extensionRuntimeStatus.messages.length)}](${createCommandUri('extension.open', this.extension.identifier.id, ExtensionEditorTab.Features)})` : undefined;
909
markdown.appendMarkdown(`$(${hasErrors ? errorIcon.id : hasWarnings ? warningIcon.id : infoIcon.id}) This extension has reported `);
910
if (errorsLink && messageLink) {
911
markdown.appendMarkdown(`${errorsLink} and ${messageLink}`);
912
} else {
913
markdown.appendMarkdown(`${errorsLink || messageLink}`);
914
}
915
markdown.appendText(`\n`);
916
}
917
}
918
919
if (extensionFeaturesAccessData.size) {
920
const registry = Registry.as<IExtensionFeaturesRegistry>(Extensions.ExtensionFeaturesRegistry);
921
for (const [featureId, accessData] of extensionFeaturesAccessData) {
922
if (accessData?.accessTimes.length) {
923
const feature = registry.getExtensionFeature(featureId);
924
if (feature) {
925
markdown.appendMarkdown(localize('feature usage label', "{0} usage", feature.label));
926
markdown.appendMarkdown(`: [${localize('total', "{0} {1} requests in last 30 days", accessData.accessTimes.length, feature.accessDataLabel ?? feature.label)}](${createCommandUri('extension.open', this.extension.identifier.id, ExtensionEditorTab.Features)})`);
927
markdown.appendText(`\n`);
928
}
929
}
930
}
931
}
932
933
for (const status of extensionStatus) {
934
if (status.icon) {
935
markdown.appendMarkdown(`$(${status.icon.id})&nbsp;`);
936
}
937
markdown.appendMarkdown(status.message.value);
938
markdown.appendText(`\n`);
939
}
940
941
if (runtimeState) {
942
markdown.appendMarkdown(`$(${infoIcon.id})&nbsp;`);
943
markdown.appendMarkdown(`${runtimeState.reason}`);
944
markdown.appendText(`\n`);
945
}
946
947
if (preReleaseMessage) {
948
const extensionPreReleaseIcon = this.themeService.getColorTheme().getColor(extensionPreReleaseIconColor);
949
markdown.appendMarkdown(`<span style="color:${extensionPreReleaseIcon ? Color.Format.CSS.formatHex(extensionPreReleaseIcon) : '#ffffff'};">$(${preReleaseIcon.id})</span>&nbsp;${preReleaseMessage}`);
950
markdown.appendText(`\n`);
951
}
952
953
if (recommendationMessage) {
954
markdown.appendMarkdown(recommendationMessage);
955
markdown.appendText(`\n`);
956
}
957
}
958
959
return markdown;
960
}
961
962
private getRecommendationMessage(extension: IExtension): string | undefined {
963
if (extension.state === ExtensionState.Installed) {
964
return undefined;
965
}
966
if (extension.deprecationInfo) {
967
return undefined;
968
}
969
const recommendation = this.extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()];
970
if (!recommendation?.reasonText) {
971
return undefined;
972
}
973
const bgColor = this.themeService.getColorTheme().getColor(extensionButtonProminentBackground);
974
return `<span style="color:${bgColor ? Color.Format.CSS.formatHex(bgColor) : '#ffffff'};">$(${starEmptyIcon.id})</span>&nbsp;${recommendation.reasonText}`;
975
}
976
977
static getPreReleaseMessage(extension: IExtension): string | undefined {
978
if (!extension.hasPreReleaseVersion) {
979
return undefined;
980
}
981
if (extension.isBuiltin) {
982
return undefined;
983
}
984
if (extension.isPreReleaseVersion) {
985
return undefined;
986
}
987
if (extension.preRelease) {
988
return undefined;
989
}
990
const preReleaseVersionLink = `[${localize('Show prerelease version', "Pre-Release version")}](${createCommandUri('workbench.extensions.action.showPreReleaseVersion', extension.identifier.id)})`;
991
return localize('has prerelease', "This extension has a {0} available", preReleaseVersionLink);
992
}
993
994
}
995
996
export class ExtensionStatusWidget extends ExtensionWidget {
997
998
private readonly renderDisposables = this._register(new MutableDisposable());
999
1000
private readonly _onDidRender = this._register(new Emitter<void>());
1001
readonly onDidRender: Event<void> = this._onDidRender.event;
1002
1003
constructor(
1004
private readonly container: HTMLElement,
1005
private readonly extensionStatusAction: ExtensionStatusAction,
1006
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
1007
) {
1008
super();
1009
this.render();
1010
this._register(extensionStatusAction.onDidChangeStatus(() => this.render()));
1011
}
1012
1013
render(): void {
1014
reset(this.container);
1015
this.renderDisposables.value = undefined;
1016
const disposables = new DisposableStore();
1017
this.renderDisposables.value = disposables;
1018
const extensionStatus = this.extensionStatusAction.status;
1019
if (extensionStatus.length) {
1020
const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true });
1021
for (let i = 0; i < extensionStatus.length; i++) {
1022
const status = extensionStatus[i];
1023
if (status.icon) {
1024
markdown.appendMarkdown(`$(${status.icon.id})&nbsp;`);
1025
}
1026
markdown.appendMarkdown(status.message.value);
1027
if (i < extensionStatus.length - 1) {
1028
markdown.appendText(`\n`);
1029
}
1030
}
1031
const rendered = disposables.add(this.markdownRendererService.render(markdown));
1032
append(this.container, rendered.element);
1033
}
1034
this._onDidRender.fire();
1035
}
1036
}
1037
1038
export class ExtensionRecommendationWidget extends ExtensionWidget {
1039
1040
private readonly _onDidRender = this._register(new Emitter<void>());
1041
readonly onDidRender: Event<void> = this._onDidRender.event;
1042
1043
constructor(
1044
private readonly container: HTMLElement,
1045
@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService,
1046
@IExtensionIgnoredRecommendationsService private readonly extensionIgnoredRecommendationsService: IExtensionIgnoredRecommendationsService,
1047
) {
1048
super();
1049
this.render();
1050
this._register(this.extensionRecommendationsService.onDidChangeRecommendations(() => this.render()));
1051
}
1052
1053
render(): void {
1054
reset(this.container);
1055
const recommendationStatus = this.getRecommendationStatus();
1056
if (recommendationStatus) {
1057
if (recommendationStatus.icon) {
1058
append(this.container, $(`div${ThemeIcon.asCSSSelector(recommendationStatus.icon)}`));
1059
}
1060
append(this.container, $(`div.recommendation-text`, undefined, recommendationStatus.message));
1061
}
1062
this._onDidRender.fire();
1063
}
1064
1065
private getRecommendationStatus(): { icon: ThemeIcon | undefined; message: string } | undefined {
1066
if (!this.extension
1067
|| this.extension.deprecationInfo
1068
|| this.extension.state === ExtensionState.Installed
1069
) {
1070
return undefined;
1071
}
1072
const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();
1073
if (extRecommendations[this.extension.identifier.id.toLowerCase()]) {
1074
const reasonText = extRecommendations[this.extension.identifier.id.toLowerCase()].reasonText;
1075
if (reasonText) {
1076
return { icon: starEmptyIcon, message: reasonText };
1077
}
1078
} else if (this.extensionIgnoredRecommendationsService.globalIgnoredRecommendations.indexOf(this.extension.identifier.id.toLowerCase()) !== -1) {
1079
return { icon: undefined, message: localize('recommendationHasBeenIgnored', "You have chosen not to receive recommendations for this extension.") };
1080
}
1081
return undefined;
1082
}
1083
}
1084
1085
export const extensionRatingIconColor = registerColor('extensionIcon.starForeground', { light: '#DF6100', dark: '#FF8E00', hcDark: '#FF8E00', hcLight: textLinkForeground }, localize('extensionIconStarForeground', "The icon color for extension ratings."), false);
1086
export const extensionPreReleaseIconColor = registerColor('extensionIcon.preReleaseForeground', { dark: '#1d9271', light: '#1d9271', hcDark: '#1d9271', hcLight: textLinkForeground }, localize('extensionPreReleaseForeground', "The icon color for pre-release extension."), false);
1087
export const extensionSponsorIconColor = registerColor('extensionIcon.sponsorForeground', { light: '#B51E78', dark: '#D758B3', hcDark: null, hcLight: '#B51E78' }, localize('extensionIcon.sponsorForeground', "The icon color for extension sponsor."), false);
1088
export const extensionPrivateBadgeBackground = registerColor('extensionIcon.privateForeground', { dark: '#ffffff60', light: '#00000060', hcDark: '#ffffff60', hcLight: '#00000060' }, localize('extensionIcon.private', "The icon color for private extensions."));
1089
1090
registerThemingParticipant((theme, collector) => {
1091
const extensionRatingIcon = theme.getColor(extensionRatingIconColor);
1092
if (extensionRatingIcon) {
1093
collector.addRule(`.extension-ratings .codicon-extensions-star-full, .extension-ratings .codicon-extensions-star-half { color: ${extensionRatingIcon}; }`);
1094
collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(starFullIcon)} { color: ${extensionRatingIcon}; }`);
1095
}
1096
1097
const extensionVerifiedPublisherIcon = theme.getColor(extensionVerifiedPublisherIconColor);
1098
if (extensionVerifiedPublisherIcon) {
1099
collector.addRule(`${ThemeIcon.asCSSSelector(verifiedPublisherIcon)} { color: ${extensionVerifiedPublisherIcon}; }`);
1100
}
1101
1102
collector.addRule(`.monaco-hover.extension-hover .markdown-hover .hover-contents ${ThemeIcon.asCSSSelector(sponsorIcon)} { color: var(--vscode-extensionIcon-sponsorForeground); }`);
1103
collector.addRule(`.extension-editor > .header > .details > .subtitle .sponsor ${ThemeIcon.asCSSSelector(sponsorIcon)} { color: var(--vscode-extensionIcon-sponsorForeground); }`);
1104
1105
const privateBadgeBackground = theme.getColor(extensionPrivateBadgeBackground);
1106
if (privateBadgeBackground) {
1107
collector.addRule(`.extension-private-badge { color: ${privateBadgeBackground}; }`);
1108
}
1109
});
1110
1111