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