Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionEditor.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 { $, Dimension, append, hide, setParentFlowTo, show } from '../../../../base/browser/dom.js';
7
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
8
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
9
import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
10
import { CheckboxActionViewItem } from '../../../../base/browser/ui/toggle/toggle.js';
11
import { Action, IAction } from '../../../../base/common/actions.js';
12
import * as arrays from '../../../../base/common/arrays.js';
13
import { Cache, CacheResult } from '../../../../base/common/cache.js';
14
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
15
import { isCancellationError } from '../../../../base/common/errors.js';
16
import { Emitter, Event } from '../../../../base/common/event.js';
17
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
18
import { Disposable, DisposableStore, MutableDisposable, dispose, toDisposable } from '../../../../base/common/lifecycle.js';
19
import { Schemas, matchesScheme } from '../../../../base/common/network.js';
20
import { isNative, language } from '../../../../base/common/platform.js';
21
import { isUndefined } from '../../../../base/common/types.js';
22
import { URI } from '../../../../base/common/uri.js';
23
import { generateUuid } from '../../../../base/common/uuid.js';
24
import './media/extensionEditor.css';
25
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
26
import { TokenizationRegistry } from '../../../../editor/common/languages.js';
27
import { ILanguageService } from '../../../../editor/common/languages/language.js';
28
import { generateTokensCSSForColorMap } from '../../../../editor/common/languages/supports/tokenization.js';
29
import { localize } from '../../../../nls.js';
30
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
31
import { ContextKeyExpr, IContextKey, IContextKeyService, IScopedContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
32
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
33
import { computeSize, FilterType, IExtensionGalleryService, IGalleryExtension, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';
34
import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
35
import { ExtensionType, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';
36
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
37
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
38
import { INotificationService } from '../../../../platform/notification/common/notification.js';
39
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
40
import { IStorageService } from '../../../../platform/storage/common/storage.js';
41
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
42
import { defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
43
import { buttonForeground, buttonHoverBackground, editorBackground, textLinkActiveForeground, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js';
44
import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
45
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
46
import { IEditorOpenContext } from '../../../common/editor.js';
47
import { ExtensionFeaturesTab } from './extensionFeaturesTab.js';
48
import {
49
ButtonWithDropDownExtensionAction,
50
ClearLanguageAction,
51
DisableDropDownAction,
52
EnableDropDownAction,
53
ButtonWithDropdownExtensionActionViewItem, DropDownExtensionAction,
54
ExtensionEditorManageExtensionAction,
55
ExtensionStatusAction,
56
ExtensionStatusLabelAction,
57
InstallAnotherVersionAction,
58
InstallDropdownAction, InstallingLabelAction,
59
LocalInstallAction,
60
MigrateDeprecatedExtensionAction,
61
ExtensionRuntimeStateAction,
62
RemoteInstallAction,
63
SetColorThemeAction,
64
SetFileIconThemeAction,
65
SetLanguageAction,
66
SetProductIconThemeAction,
67
ToggleAutoUpdateForExtensionAction,
68
UninstallAction,
69
UpdateAction,
70
WebInstallAction,
71
TogglePreReleaseExtensionAction,
72
} from './extensionsActions.js';
73
import { Delegate } from './extensionsList.js';
74
import { ExtensionData, ExtensionsGridView, ExtensionsTree, getExtensions } from './extensionsViewer.js';
75
import { ExtensionRecommendationWidget, ExtensionStatusWidget, ExtensionWidget, InstallCountWidget, RatingsWidget, RemoteBadgeWidget, SponsorWidget, PublisherWidget, onClick, ExtensionKindIndicatorWidget, ExtensionIconWidget } from './extensionsWidgets.js';
76
import { ExtensionContainers, ExtensionEditorTab, ExtensionState, IExtension, IExtensionContainer, IExtensionsWorkbenchService } from '../common/extensions.js';
77
import { ExtensionsInput, IExtensionEditorOptions } from '../common/extensionsInput.js';
78
import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from '../../markdown/browser/markdownDocumentRenderer.js';
79
import { IWebview, IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED } from '../../webview/browser/webview.js';
80
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
81
import { IEditorService } from '../../../services/editor/common/editorService.js';
82
import { IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';
83
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
84
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
85
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
86
import { ByteSize, IFileService } from '../../../../platform/files/common/files.js';
87
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
88
import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js';
89
import { IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
90
import { ShowCurrentReleaseNotesActionId } from '../../update/common/update.js';
91
92
function toDateString(date: Date) {
93
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}, ${date.toLocaleTimeString(language, { hourCycle: 'h23' })}`;
94
}
95
96
class NavBar extends Disposable {
97
98
private _onChange = this._register(new Emitter<{ id: string | null; focus: boolean }>());
99
get onChange(): Event<{ id: string | null; focus: boolean }> { return this._onChange.event; }
100
101
private _currentId: string | null = null;
102
get currentId(): string | null { return this._currentId; }
103
104
private actions: Action[];
105
private actionbar: ActionBar;
106
107
constructor(container: HTMLElement) {
108
super();
109
const element = append(container, $('.navbar'));
110
this.actions = [];
111
this.actionbar = this._register(new ActionBar(element));
112
}
113
114
push(id: string, label: string, tooltip: string): void {
115
const action = new Action(id, label, undefined, true, () => this.update(id, true));
116
117
action.tooltip = tooltip;
118
119
this.actions.push(action);
120
this.actionbar.push(action);
121
122
if (this.actions.length === 1) {
123
this.update(id);
124
}
125
}
126
127
clear(): void {
128
this.actions = dispose(this.actions);
129
this.actionbar.clear();
130
}
131
132
switch(id: string): boolean {
133
const action = this.actions.find(action => action.id === id);
134
if (action) {
135
action.run();
136
return true;
137
}
138
return false;
139
}
140
141
private update(id: string, focus?: boolean): void {
142
this._currentId = id;
143
this._onChange.fire({ id, focus: !!focus });
144
this.actions.forEach(a => a.checked = a.id === id);
145
}
146
}
147
148
interface ILayoutParticipant {
149
layout(): void;
150
}
151
152
interface IActiveElement {
153
focus(): void;
154
}
155
156
interface IExtensionEditorTemplate {
157
name: HTMLElement;
158
preview: HTMLElement;
159
builtin: HTMLElement;
160
description: HTMLElement;
161
actionsAndStatusContainer: HTMLElement;
162
extensionActionBar: ActionBar;
163
navbar: NavBar;
164
content: HTMLElement;
165
header: HTMLElement;
166
extension: IExtension;
167
gallery: IGalleryExtension | null;
168
manifest: IExtensionManifest | null;
169
}
170
171
const enum WebviewIndex {
172
Readme,
173
Changelog
174
}
175
176
const CONTEXT_SHOW_PRE_RELEASE_VERSION = new RawContextKey<boolean>('showPreReleaseVersion', false);
177
178
abstract class ExtensionWithDifferentGalleryVersionWidget extends ExtensionWidget {
179
private _gallery: IGalleryExtension | null = null;
180
get gallery(): IGalleryExtension | null { return this._gallery; }
181
set gallery(gallery: IGalleryExtension | null) {
182
if (this.extension && gallery && !areSameExtensions(this.extension.identifier, gallery.identifier)) {
183
return;
184
}
185
this._gallery = gallery;
186
this.update();
187
}
188
}
189
190
class VersionWidget extends ExtensionWithDifferentGalleryVersionWidget {
191
private readonly element: HTMLElement;
192
constructor(
193
container: HTMLElement,
194
hoverService: IHoverService
195
) {
196
super();
197
this.element = append(container, $('code.version', undefined, 'pre-release'));
198
this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, localize('extension version', "Extension Version")));
199
this.render();
200
}
201
render(): void {
202
if (this.extension?.preRelease) {
203
show(this.element);
204
} else {
205
hide(this.element);
206
}
207
}
208
}
209
210
export class ExtensionEditor extends EditorPane {
211
212
static readonly ID: string = 'workbench.editor.extension';
213
214
private readonly _scopedContextKeyService = this._register(new MutableDisposable<IScopedContextKeyService>());
215
private template: IExtensionEditorTemplate | undefined;
216
217
private extensionReadme: Cache<string> | null;
218
private extensionChangelog: Cache<string> | null;
219
private extensionManifest: Cache<IExtensionManifest | null> | null;
220
221
// Some action bar items use a webview whose vertical scroll position we track in this map
222
private initialScrollProgress: Map<WebviewIndex, number> = new Map();
223
224
// Spot when an ExtensionEditor instance gets reused for a different extension, in which case the vertical scroll positions must be zeroed
225
private currentIdentifier: string = '';
226
227
private layoutParticipants: ILayoutParticipant[] = [];
228
private readonly contentDisposables = this._register(new DisposableStore());
229
private readonly transientDisposables = this._register(new DisposableStore());
230
private activeElement: IActiveElement | null = null;
231
private dimension: Dimension | undefined;
232
233
private showPreReleaseVersionContextKey: IContextKey<boolean> | undefined;
234
235
constructor(
236
group: IEditorGroup,
237
@ITelemetryService telemetryService: ITelemetryService,
238
@IInstantiationService private readonly instantiationService: IInstantiationService,
239
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
240
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
241
@IThemeService themeService: IThemeService,
242
@INotificationService private readonly notificationService: INotificationService,
243
@IOpenerService private readonly openerService: IOpenerService,
244
@IExtensionRecommendationsService private readonly extensionRecommendationsService: IExtensionRecommendationsService,
245
@IStorageService storageService: IStorageService,
246
@IExtensionService private readonly extensionService: IExtensionService,
247
@IWebviewService private readonly webviewService: IWebviewService,
248
@ILanguageService private readonly languageService: ILanguageService,
249
@IContextMenuService private readonly contextMenuService: IContextMenuService,
250
@IContextKeyService private readonly contextKeyService: IContextKeyService,
251
@IHoverService private readonly hoverService: IHoverService,
252
) {
253
super(ExtensionEditor.ID, group, telemetryService, themeService, storageService);
254
this.extensionReadme = null;
255
this.extensionChangelog = null;
256
this.extensionManifest = null;
257
}
258
259
override get scopedContextKeyService(): IContextKeyService | undefined {
260
return this._scopedContextKeyService.value;
261
}
262
263
protected createEditor(parent: HTMLElement): void {
264
const root = append(parent, $('.extension-editor'));
265
this._scopedContextKeyService.value = this.contextKeyService.createScoped(root);
266
this._scopedContextKeyService.value.createKey('inExtensionEditor', true);
267
this.showPreReleaseVersionContextKey = CONTEXT_SHOW_PRE_RELEASE_VERSION.bindTo(this._scopedContextKeyService.value);
268
269
root.tabIndex = 0; // this is required for the focus tracker on the editor
270
root.style.outline = 'none';
271
root.setAttribute('role', 'document');
272
const header = append(root, $('.header'));
273
274
const iconContainer = append(header, $('.icon-container'));
275
const iconWidget = this.instantiationService.createInstance(ExtensionIconWidget, iconContainer);
276
const remoteBadge = this.instantiationService.createInstance(RemoteBadgeWidget, iconContainer, true);
277
278
const details = append(header, $('.details'));
279
const title = append(details, $('.title'));
280
const name = append(title, $('span.name.clickable', { role: 'heading', tabIndex: 0 }));
281
this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), name, localize('name', "Extension name")));
282
const versionWidget = new VersionWidget(title, this.hoverService);
283
284
const preview = append(title, $('span.preview'));
285
this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), preview, localize('preview', "Preview")));
286
preview.textContent = localize('preview', "Preview");
287
288
const builtin = append(title, $('span.builtin'));
289
builtin.textContent = localize('builtin', "Built-in");
290
291
const subtitle = append(details, $('.subtitle'));
292
const subTitleEntryContainers: HTMLElement[] = [];
293
294
const publisherContainer = append(subtitle, $('.subtitle-entry'));
295
subTitleEntryContainers.push(publisherContainer);
296
const publisherWidget = this.instantiationService.createInstance(PublisherWidget, publisherContainer, false);
297
298
const extensionKindContainer = append(subtitle, $('.subtitle-entry'));
299
subTitleEntryContainers.push(extensionKindContainer);
300
const extensionKindWidget = this.instantiationService.createInstance(ExtensionKindIndicatorWidget, extensionKindContainer, false);
301
302
const installCountContainer = append(subtitle, $('.subtitle-entry'));
303
subTitleEntryContainers.push(installCountContainer);
304
const installCountWidget = this.instantiationService.createInstance(InstallCountWidget, installCountContainer, false);
305
306
const ratingsContainer = append(subtitle, $('.subtitle-entry'));
307
subTitleEntryContainers.push(ratingsContainer);
308
const ratingsWidget = this.instantiationService.createInstance(RatingsWidget, ratingsContainer, false);
309
310
const sponsorContainer = append(subtitle, $('.subtitle-entry'));
311
subTitleEntryContainers.push(sponsorContainer);
312
const sponsorWidget = this.instantiationService.createInstance(SponsorWidget, sponsorContainer);
313
314
const widgets: ExtensionWidget[] = [
315
iconWidget,
316
remoteBadge,
317
versionWidget,
318
publisherWidget,
319
extensionKindWidget,
320
installCountWidget,
321
ratingsWidget,
322
sponsorWidget,
323
];
324
325
const description = append(details, $('.description'));
326
327
const installAction = this.instantiationService.createInstance(InstallDropdownAction);
328
const actions = [
329
this.instantiationService.createInstance(ExtensionRuntimeStateAction),
330
this.instantiationService.createInstance(ExtensionStatusLabelAction),
331
this.instantiationService.createInstance(UpdateAction, true),
332
this.instantiationService.createInstance(SetColorThemeAction),
333
this.instantiationService.createInstance(SetFileIconThemeAction),
334
this.instantiationService.createInstance(SetProductIconThemeAction),
335
this.instantiationService.createInstance(SetLanguageAction),
336
this.instantiationService.createInstance(ClearLanguageAction),
337
338
this.instantiationService.createInstance(EnableDropDownAction),
339
this.instantiationService.createInstance(DisableDropDownAction),
340
this.instantiationService.createInstance(RemoteInstallAction, false),
341
this.instantiationService.createInstance(LocalInstallAction),
342
this.instantiationService.createInstance(WebInstallAction),
343
installAction,
344
this.instantiationService.createInstance(InstallingLabelAction),
345
this.instantiationService.createInstance(ButtonWithDropDownExtensionAction, 'extensions.uninstall', UninstallAction.UninstallClass, [
346
[
347
this.instantiationService.createInstance(MigrateDeprecatedExtensionAction, false),
348
this.instantiationService.createInstance(UninstallAction),
349
this.instantiationService.createInstance(InstallAnotherVersionAction, null, true),
350
]
351
]),
352
this.instantiationService.createInstance(TogglePreReleaseExtensionAction),
353
this.instantiationService.createInstance(ToggleAutoUpdateForExtensionAction),
354
new ExtensionEditorManageExtensionAction(this.scopedContextKeyService || this.contextKeyService, this.instantiationService),
355
];
356
357
const actionsAndStatusContainer = append(details, $('.actions-status-container'));
358
const extensionActionBar = this._register(new ActionBar(actionsAndStatusContainer, {
359
actionViewItemProvider: (action: IAction, options) => {
360
if (action instanceof DropDownExtensionAction) {
361
return action.createActionViewItem(options);
362
}
363
if (action instanceof ButtonWithDropDownExtensionAction) {
364
return new ButtonWithDropdownExtensionActionViewItem(
365
action,
366
{
367
...options,
368
icon: true,
369
label: true,
370
menuActionsOrProvider: { getActions: () => action.menuActions },
371
menuActionClassNames: action.menuActionClassNames
372
},
373
this.contextMenuService);
374
}
375
if (action instanceof ToggleAutoUpdateForExtensionAction) {
376
return new CheckboxActionViewItem(undefined, action, { ...options, icon: true, label: true, checkboxStyles: defaultCheckboxStyles });
377
}
378
return undefined;
379
},
380
focusOnlyEnabledItems: true
381
}));
382
383
extensionActionBar.push(actions, { icon: true, label: true });
384
extensionActionBar.setFocusable(true);
385
// update focusable elements when the enablement of an action changes
386
this._register(Event.any(...actions.map(a => Event.filter(a.onDidChange, e => e.enabled !== undefined)))(() => {
387
extensionActionBar.setFocusable(false);
388
extensionActionBar.setFocusable(true);
389
}));
390
391
const otherExtensionContainers: IExtensionContainer[] = [];
392
const extensionStatusAction = this.instantiationService.createInstance(ExtensionStatusAction);
393
const extensionStatusWidget = this._register(this.instantiationService.createInstance(ExtensionStatusWidget, append(actionsAndStatusContainer, $('.status')), extensionStatusAction));
394
395
otherExtensionContainers.push(extensionStatusAction, new class extends ExtensionWidget {
396
render() {
397
actionsAndStatusContainer.classList.toggle('list-layout', this.extension?.state === ExtensionState.Installed);
398
}
399
}());
400
401
const recommendationWidget = this.instantiationService.createInstance(ExtensionRecommendationWidget, append(details, $('.recommendation')));
402
widgets.push(recommendationWidget);
403
404
this._register(Event.any(extensionStatusWidget.onDidRender, recommendationWidget.onDidRender)(() => {
405
if (this.dimension) {
406
this.layout(this.dimension);
407
}
408
}));
409
410
const extensionContainers: ExtensionContainers = this.instantiationService.createInstance(ExtensionContainers, [...actions, ...widgets, ...otherExtensionContainers]);
411
for (const disposable of [...actions, ...widgets, ...otherExtensionContainers, extensionContainers]) {
412
this._register(disposable);
413
}
414
415
const onError = Event.chain(extensionActionBar.onDidRun, $ =>
416
$.map(({ error }) => error)
417
.filter(error => !!error)
418
);
419
420
this._register(onError(this.onError, this));
421
422
const body = append(root, $('.body'));
423
const navbar = new NavBar(body);
424
425
const content = append(body, $('.content'));
426
content.id = generateUuid(); // An id is needed for the webview parent flow to
427
428
this.template = {
429
builtin,
430
content,
431
description,
432
header,
433
name,
434
navbar,
435
preview,
436
actionsAndStatusContainer,
437
extensionActionBar,
438
set extension(extension: IExtension) {
439
extensionContainers.extension = extension;
440
let lastNonEmptySubtitleEntryContainer;
441
for (const subTitleEntryElement of subTitleEntryContainers) {
442
subTitleEntryElement.classList.remove('last-non-empty');
443
if (subTitleEntryElement.children.length > 0) {
444
lastNonEmptySubtitleEntryContainer = subTitleEntryElement;
445
}
446
}
447
if (lastNonEmptySubtitleEntryContainer) {
448
lastNonEmptySubtitleEntryContainer.classList.add('last-non-empty');
449
}
450
},
451
set gallery(gallery: IGalleryExtension | null) {
452
versionWidget.gallery = gallery;
453
},
454
set manifest(manifest: IExtensionManifest | null) {
455
installAction.manifest = manifest;
456
}
457
};
458
}
459
460
override async setInput(input: ExtensionsInput, options: IExtensionEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<void> {
461
await super.setInput(input, options, context, token);
462
this.updatePreReleaseVersionContext();
463
if (this.template) {
464
await this.render(input.extension, this.template, !!options?.preserveFocus);
465
}
466
}
467
468
override setOptions(options: IExtensionEditorOptions | undefined): void {
469
const currentOptions: IExtensionEditorOptions | undefined = this.options;
470
super.setOptions(options);
471
this.updatePreReleaseVersionContext();
472
473
if (this.input && this.template && currentOptions?.showPreReleaseVersion !== options?.showPreReleaseVersion) {
474
this.render((this.input as ExtensionsInput).extension, this.template, !!options?.preserveFocus);
475
return;
476
}
477
478
if (options?.tab) {
479
this.template?.navbar.switch(options.tab);
480
}
481
482
}
483
484
private updatePreReleaseVersionContext(): void {
485
let showPreReleaseVersion = (<IExtensionEditorOptions | undefined>this.options)?.showPreReleaseVersion;
486
if (isUndefined(showPreReleaseVersion)) {
487
showPreReleaseVersion = !!(<ExtensionsInput>this.input).extension.gallery?.properties.isPreReleaseVersion;
488
}
489
this.showPreReleaseVersionContextKey?.set(showPreReleaseVersion);
490
}
491
492
async openTab(tab: ExtensionEditorTab): Promise<void> {
493
if (!this.input || !this.template) {
494
return;
495
}
496
if (this.template.navbar.switch(tab)) {
497
return;
498
}
499
// Fallback to Readme tab if ExtensionPack tab does not exist
500
if (tab === ExtensionEditorTab.ExtensionPack) {
501
this.template.navbar.switch(ExtensionEditorTab.Readme);
502
}
503
}
504
505
private async getGalleryVersionToShow(extension: IExtension, preRelease?: boolean): Promise<IGalleryExtension | null> {
506
if (extension.resourceExtension) {
507
return null;
508
}
509
if (extension.local?.source === 'resource') {
510
return null;
511
}
512
if (isUndefined(preRelease)) {
513
return null;
514
}
515
if (preRelease === extension.gallery?.properties.isPreReleaseVersion) {
516
return null;
517
}
518
if (preRelease && !extension.hasPreReleaseVersion) {
519
return null;
520
}
521
if (!preRelease && !extension.hasReleaseVersion) {
522
return null;
523
}
524
return (await this.extensionGalleryService.getExtensions([{ ...extension.identifier, preRelease, hasPreRelease: extension.hasPreReleaseVersion }], CancellationToken.None))[0] || null;
525
}
526
527
private async render(extension: IExtension, template: IExtensionEditorTemplate, preserveFocus: boolean): Promise<void> {
528
this.activeElement = null;
529
this.transientDisposables.clear();
530
531
const token = this.transientDisposables.add(new CancellationTokenSource()).token;
532
533
const gallery = await this.getGalleryVersionToShow(extension, (this.options as IExtensionEditorOptions)?.showPreReleaseVersion);
534
if (token.isCancellationRequested) {
535
return;
536
}
537
538
this.extensionReadme = new Cache(() => gallery ? this.extensionGalleryService.getReadme(gallery, token) : extension.getReadme(token));
539
this.extensionChangelog = new Cache(() => gallery ? this.extensionGalleryService.getChangelog(gallery, token) : extension.getChangelog(token));
540
this.extensionManifest = new Cache(() => gallery ? this.extensionGalleryService.getManifest(gallery, token) : extension.getManifest(token));
541
542
template.extension = extension;
543
template.gallery = gallery;
544
template.manifest = null;
545
546
template.name.textContent = extension.displayName;
547
template.name.classList.toggle('clickable', !!extension.url);
548
template.name.classList.toggle('deprecated', !!extension.deprecationInfo);
549
template.preview.style.display = extension.preview ? 'inherit' : 'none';
550
template.builtin.style.display = extension.isBuiltin ? 'inherit' : 'none';
551
552
template.description.textContent = extension.description;
553
554
if (extension.url) {
555
this.transientDisposables.add(onClick(template.name, () => this.openerService.open(URI.parse(extension.url!))));
556
}
557
558
const manifest = await this.extensionManifest.get().promise;
559
if (token.isCancellationRequested) {
560
return;
561
}
562
563
if (manifest) {
564
template.manifest = manifest;
565
}
566
567
this.renderNavbar(extension, manifest, template, preserveFocus);
568
569
// report telemetry
570
const extRecommendations = this.extensionRecommendationsService.getAllRecommendationsWithReason();
571
let recommendationsData = {};
572
if (extRecommendations[extension.identifier.id.toLowerCase()]) {
573
recommendationsData = { recommendationReason: extRecommendations[extension.identifier.id.toLowerCase()].reasonId };
574
}
575
/* __GDPR__
576
"extensionGallery:openExtension" : {
577
"owner": "sandy081",
578
"recommendationReason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
579
"${include}": [
580
"${GalleryExtensionTelemetryData}"
581
]
582
}
583
*/
584
this.telemetryService.publicLog('extensionGallery:openExtension', { ...extension.telemetryData, ...recommendationsData });
585
586
}
587
588
private renderNavbar(extension: IExtension, manifest: IExtensionManifest | null, template: IExtensionEditorTemplate, preserveFocus: boolean): void {
589
template.content.innerText = '';
590
template.navbar.clear();
591
592
if (this.currentIdentifier !== extension.identifier.id) {
593
this.initialScrollProgress.clear();
594
this.currentIdentifier = extension.identifier.id;
595
}
596
597
template.navbar.push(ExtensionEditorTab.Readme, localize('details', "Details"), localize('detailstooltip', "Extension details, rendered from the extension's 'README.md' file"));
598
if (manifest) {
599
template.navbar.push(ExtensionEditorTab.Features, localize('features', "Features"), localize('featurestooltip', "Lists features contributed by this extension"));
600
}
601
if (extension.hasChangelog()) {
602
template.navbar.push(ExtensionEditorTab.Changelog, localize('changelog', "Changelog"), localize('changelogtooltip', "Extension update history, rendered from the extension's 'CHANGELOG.md' file"));
603
}
604
if (extension.dependencies.length) {
605
template.navbar.push(ExtensionEditorTab.Dependencies, localize('dependencies', "Dependencies"), localize('dependenciestooltip', "Lists extensions this extension depends on"));
606
}
607
if (manifest && manifest.extensionPack?.length && !this.shallRenderAsExtensionPack(manifest)) {
608
template.navbar.push(ExtensionEditorTab.ExtensionPack, localize('extensionpack', "Extension Pack"), localize('extensionpacktooltip', "Lists extensions those will be installed together with this extension"));
609
}
610
611
if ((<IExtensionEditorOptions | undefined>this.options)?.tab) {
612
template.navbar.switch((<IExtensionEditorOptions>this.options).tab!);
613
}
614
if (template.navbar.currentId) {
615
this.onNavbarChange(extension, { id: template.navbar.currentId, focus: !preserveFocus }, template);
616
}
617
template.navbar.onChange(e => this.onNavbarChange(extension, e, template), this, this.transientDisposables);
618
}
619
620
override clearInput(): void {
621
this.contentDisposables.clear();
622
this.transientDisposables.clear();
623
624
super.clearInput();
625
}
626
627
override focus(): void {
628
super.focus();
629
this.activeElement?.focus();
630
}
631
632
showFind(): void {
633
this.activeWebview?.showFind();
634
}
635
636
runFindAction(previous: boolean): void {
637
this.activeWebview?.runFindAction(previous);
638
}
639
640
public get activeWebview(): IWebview | undefined {
641
if (!this.activeElement || !(this.activeElement as IWebview).runFindAction) {
642
return undefined;
643
}
644
return this.activeElement as IWebview;
645
}
646
647
private onNavbarChange(extension: IExtension, { id, focus }: { id: string | null; focus: boolean }, template: IExtensionEditorTemplate): void {
648
this.contentDisposables.clear();
649
template.content.innerText = '';
650
this.activeElement = null;
651
if (id) {
652
const cts = new CancellationTokenSource();
653
this.contentDisposables.add(toDisposable(() => cts.dispose(true)));
654
this.open(id, extension, template, cts.token)
655
.then(activeElement => {
656
if (cts.token.isCancellationRequested) {
657
return;
658
}
659
this.activeElement = activeElement;
660
if (focus) {
661
this.focus();
662
}
663
});
664
}
665
}
666
667
private open(id: string, extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
668
switch (id) {
669
case ExtensionEditorTab.Readme: return this.openDetails(extension, template, token);
670
case ExtensionEditorTab.Features: return this.openFeatures(template, token);
671
case ExtensionEditorTab.Changelog: return this.openChangelog(extension, template, token);
672
case ExtensionEditorTab.Dependencies: return this.openExtensionDependencies(extension, template, token);
673
case ExtensionEditorTab.ExtensionPack: return this.openExtensionPack(extension, template, token);
674
}
675
return Promise.resolve(null);
676
}
677
678
private async openMarkdown(extension: IExtension, cacheResult: CacheResult<string>, noContentCopy: string, container: HTMLElement, webviewIndex: WebviewIndex, title: string, token: CancellationToken): Promise<IActiveElement | null> {
679
try {
680
const body = await this.renderMarkdown(extension, cacheResult, container, token);
681
if (token.isCancellationRequested) {
682
return Promise.resolve(null);
683
}
684
685
const webview = this.contentDisposables.add(this.webviewService.createWebviewOverlay({
686
title,
687
options: {
688
enableFindWidget: true,
689
tryRestoreScrollPosition: true,
690
disableServiceWorker: true,
691
},
692
contentOptions: {},
693
extension: undefined,
694
}));
695
696
webview.initialScrollProgress = this.initialScrollProgress.get(webviewIndex) || 0;
697
698
webview.claim(this, this.window, this.scopedContextKeyService);
699
setParentFlowTo(webview.container, container);
700
webview.layoutWebviewOverElement(container);
701
702
webview.setHtml(body);
703
webview.claim(this, this.window, undefined);
704
705
this.contentDisposables.add(webview.onDidFocus(() => this._onDidFocus?.fire()));
706
707
this.contentDisposables.add(webview.onDidScroll(() => this.initialScrollProgress.set(webviewIndex, webview.initialScrollProgress)));
708
709
const removeLayoutParticipant = arrays.insert(this.layoutParticipants, {
710
layout: () => {
711
webview.layoutWebviewOverElement(container);
712
}
713
});
714
this.contentDisposables.add(toDisposable(removeLayoutParticipant));
715
716
let isDisposed = false;
717
this.contentDisposables.add(toDisposable(() => { isDisposed = true; }));
718
719
this.contentDisposables.add(this.themeService.onDidColorThemeChange(async () => {
720
// Render again since syntax highlighting of code blocks may have changed
721
const body = await this.renderMarkdown(extension, cacheResult, container);
722
if (!isDisposed) { // Make sure we weren't disposed of in the meantime
723
webview.setHtml(body);
724
}
725
}));
726
727
this.contentDisposables.add(webview.onDidClickLink(link => {
728
if (!link) {
729
return;
730
}
731
// Only allow links with specific schemes
732
if (matchesScheme(link, Schemas.http) || matchesScheme(link, Schemas.https) || matchesScheme(link, Schemas.mailto)) {
733
this.openerService.open(link);
734
} else if (matchesScheme(link, Schemas.command) && extension.type === ExtensionType.System) {
735
this.openerService.open(link, {
736
allowCommands: [
737
ShowCurrentReleaseNotesActionId
738
]
739
});
740
}
741
}));
742
743
return webview;
744
} catch (e) {
745
const p = append(container, $('p.nocontent'));
746
p.textContent = noContentCopy;
747
return p;
748
}
749
}
750
751
private async renderMarkdown(extension: IExtension, cacheResult: CacheResult<string>, container: HTMLElement, token?: CancellationToken): Promise<string> {
752
const contents = await this.loadContents(() => cacheResult, container);
753
if (token?.isCancellationRequested) {
754
return '';
755
}
756
757
const allowedLinkProtocols = [Schemas.http, Schemas.https, Schemas.mailto];
758
const content = await renderMarkdownDocument(contents, this.extensionService, this.languageService, {
759
sanitizerConfig: {
760
allowedLinkProtocols: {
761
override: extension.type === ExtensionType.System
762
? [...allowedLinkProtocols, Schemas.command]
763
: allowedLinkProtocols
764
}
765
}
766
}, token);
767
if (token?.isCancellationRequested) {
768
return '';
769
}
770
771
return this.renderBody(content);
772
}
773
774
private renderBody(body: TrustedHTML): string {
775
const nonce = generateUuid();
776
const colorMap = TokenizationRegistry.getColorMap();
777
const css = colorMap ? generateTokensCSSForColorMap(colorMap) : '';
778
return `<!DOCTYPE html>
779
<html>
780
<head>
781
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
782
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: data:; media-src https:; script-src 'none'; style-src 'nonce-${nonce}';">
783
<style nonce="${nonce}">
784
${DEFAULT_MARKDOWN_STYLES}
785
786
/* prevent scroll-to-top button from blocking the body text */
787
body {
788
padding-bottom: 75px;
789
}
790
791
#scroll-to-top {
792
position: fixed;
793
width: 32px;
794
height: 32px;
795
right: 25px;
796
bottom: 25px;
797
background-color: var(--vscode-button-secondaryBackground);
798
border-color: var(--vscode-button-border);
799
border-radius: 50%;
800
cursor: pointer;
801
box-shadow: 1px 1px 1px rgba(0,0,0,.25);
802
outline: none;
803
display: flex;
804
justify-content: center;
805
align-items: center;
806
}
807
808
#scroll-to-top:hover {
809
background-color: var(--vscode-button-secondaryHoverBackground);
810
box-shadow: 2px 2px 2px rgba(0,0,0,.25);
811
}
812
813
body.vscode-high-contrast #scroll-to-top {
814
border-width: 2px;
815
border-style: solid;
816
box-shadow: none;
817
}
818
819
#scroll-to-top span.icon::before {
820
content: "";
821
display: block;
822
background: var(--vscode-button-secondaryForeground);
823
/* Chevron up icon */
824
webkit-mask-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNiAxNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTYgMTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkZGRkZGO30KCS5zdDF7ZmlsbDpub25lO30KPC9zdHlsZT4KPHRpdGxlPnVwY2hldnJvbjwvdGl0bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik04LDUuMWwtNy4zLDcuM0wwLDExLjZsOC04bDgsOGwtMC43LDAuN0w4LDUuMXoiLz4KPHJlY3QgY2xhc3M9InN0MSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+Cjwvc3ZnPgo=');
825
-webkit-mask-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNiAxNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTYgMTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkZGRkZGO30KCS5zdDF7ZmlsbDpub25lO30KPC9zdHlsZT4KPHRpdGxlPnVwY2hldnJvbjwvdGl0bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik04LDUuMWwtNy4zLDcuM0wwLDExLjZsOC04bDgsOGwtMC43LDAuN0w4LDUuMXoiLz4KPHJlY3QgY2xhc3M9InN0MSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+Cjwvc3ZnPgo=');
826
width: 16px;
827
height: 16px;
828
}
829
${css}
830
</style>
831
</head>
832
<body>
833
<a id="scroll-to-top" role="button" aria-label="scroll to top" href="#"><span class="icon"></span></a>
834
${body}
835
</body>
836
</html>`;
837
}
838
839
private async openDetails(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
840
const details = append(template.content, $('.details'));
841
const readmeContainer = append(details, $('.readme-container'));
842
const additionalDetailsContainer = append(details, $('.additional-details-container'));
843
844
const layout = () => details.classList.toggle('narrow', this.dimension && this.dimension.width < 500);
845
layout();
846
this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout })));
847
848
let activeElement: IActiveElement | null = null;
849
const manifest = await this.extensionManifest!.get().promise;
850
if (manifest && manifest.extensionPack?.length && this.shallRenderAsExtensionPack(manifest)) {
851
activeElement = await this.openExtensionPackReadme(extension, manifest, readmeContainer, token);
852
} else {
853
activeElement = await this.openMarkdown(extension, this.extensionReadme!.get(), localize('noReadme', "No README available."), readmeContainer, WebviewIndex.Readme, localize('Readme title', "Readme"), token);
854
}
855
856
this.renderAdditionalDetails(additionalDetailsContainer, extension);
857
return activeElement;
858
}
859
860
private shallRenderAsExtensionPack(manifest: IExtensionManifest): boolean {
861
return !!(manifest.categories?.some(category => category.toLowerCase() === 'extension packs'));
862
}
863
864
private async openExtensionPackReadme(extension: IExtension, manifest: IExtensionManifest, container: HTMLElement, token: CancellationToken): Promise<IActiveElement | null> {
865
if (token.isCancellationRequested) {
866
return Promise.resolve(null);
867
}
868
869
const extensionPackReadme = append(container, $('div', { class: 'extension-pack-readme' }));
870
extensionPackReadme.style.margin = '0 auto';
871
extensionPackReadme.style.maxWidth = '882px';
872
873
const extensionPack = append(extensionPackReadme, $('div', { class: 'extension-pack' }));
874
if (manifest.extensionPack!.length <= 3) {
875
extensionPackReadme.classList.add('one-row');
876
} else if (manifest.extensionPack!.length <= 6) {
877
extensionPackReadme.classList.add('two-rows');
878
} else if (manifest.extensionPack!.length <= 9) {
879
extensionPackReadme.classList.add('three-rows');
880
} else {
881
extensionPackReadme.classList.add('more-rows');
882
}
883
884
const extensionPackHeader = append(extensionPack, $('div.header'));
885
extensionPackHeader.textContent = localize('extension pack', "Extension Pack ({0})", manifest.extensionPack!.length);
886
const extensionPackContent = append(extensionPack, $('div', { class: 'extension-pack-content' }));
887
extensionPackContent.setAttribute('tabindex', '0');
888
append(extensionPack, $('div.footer'));
889
const readmeContent = append(extensionPackReadme, $('div.readme-content'));
890
891
await Promise.all([
892
this.renderExtensionPack(manifest, extensionPackContent, token),
893
this.openMarkdown(extension, this.extensionReadme!.get(), localize('noReadme', "No README available."), readmeContent, WebviewIndex.Readme, localize('Readme title', "Readme"), token),
894
]);
895
896
return { focus: () => extensionPackContent.focus() };
897
}
898
899
private renderAdditionalDetails(container: HTMLElement, extension: IExtension): void {
900
const content = $('div', { class: 'additional-details-content', tabindex: '0' });
901
const scrollableContent = new DomScrollableElement(content, {});
902
const layout = () => scrollableContent.scanDomNode();
903
const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout });
904
this.contentDisposables.add(toDisposable(removeLayoutParticipant));
905
this.contentDisposables.add(scrollableContent);
906
907
this.contentDisposables.add(this.instantiationService.createInstance(AdditionalDetailsWidget, content, extension));
908
909
append(container, scrollableContent.getDomNode());
910
scrollableContent.scanDomNode();
911
}
912
913
private openChangelog(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
914
return this.openMarkdown(extension, this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template.content, WebviewIndex.Changelog, localize('Changelog title', "Changelog"), token);
915
}
916
917
private async openFeatures(template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
918
const manifest = await this.loadContents(() => this.extensionManifest!.get(), template.content);
919
if (token.isCancellationRequested) {
920
return null;
921
}
922
if (!manifest) {
923
return null;
924
}
925
926
const extensionFeaturesTab = this.contentDisposables.add(this.instantiationService.createInstance(ExtensionFeaturesTab, manifest, (<IExtensionEditorOptions | undefined>this.options)?.feature));
927
const layout = () => extensionFeaturesTab.layout(template.content.clientHeight, template.content.clientWidth);
928
const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout });
929
this.contentDisposables.add(toDisposable(removeLayoutParticipant));
930
append(template.content, extensionFeaturesTab.domNode);
931
layout();
932
return extensionFeaturesTab.domNode;
933
}
934
935
private openExtensionDependencies(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
936
if (token.isCancellationRequested) {
937
return Promise.resolve(null);
938
}
939
940
if (arrays.isFalsyOrEmpty(extension.dependencies)) {
941
append(template.content, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies");
942
return Promise.resolve(template.content);
943
}
944
945
const content = $('div', { class: 'subcontent' });
946
const scrollableContent = new DomScrollableElement(content, {});
947
append(template.content, scrollableContent.getDomNode());
948
this.contentDisposables.add(scrollableContent);
949
950
const dependenciesTree = this.instantiationService.createInstance(ExtensionsTree,
951
new ExtensionData(extension, null, extension => extension.dependencies || [], this.extensionsWorkbenchService), content,
952
{
953
listBackground: editorBackground
954
});
955
const layout = () => {
956
scrollableContent.scanDomNode();
957
const scrollDimensions = scrollableContent.getScrollDimensions();
958
dependenciesTree.layout(scrollDimensions.height);
959
};
960
const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout });
961
this.contentDisposables.add(toDisposable(removeLayoutParticipant));
962
963
this.contentDisposables.add(dependenciesTree);
964
scrollableContent.scanDomNode();
965
return Promise.resolve({ focus() { dependenciesTree.domFocus(); } });
966
}
967
968
private async openExtensionPack(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise<IActiveElement | null> {
969
if (token.isCancellationRequested) {
970
return Promise.resolve(null);
971
}
972
const manifest = await this.loadContents(() => this.extensionManifest!.get(), template.content);
973
if (token.isCancellationRequested) {
974
return null;
975
}
976
if (!manifest) {
977
return null;
978
}
979
return this.renderExtensionPack(manifest, template.content, token);
980
}
981
982
private async renderExtensionPack(manifest: IExtensionManifest, parent: HTMLElement, token: CancellationToken): Promise<IActiveElement | null> {
983
if (token.isCancellationRequested) {
984
return null;
985
}
986
987
const content = $('div', { class: 'subcontent' });
988
const scrollableContent = new DomScrollableElement(content, { useShadows: false });
989
append(parent, scrollableContent.getDomNode());
990
991
const extensionsGridView = this.instantiationService.createInstance(ExtensionsGridView, content, new Delegate());
992
const extensions: IExtension[] = await getExtensions(manifest.extensionPack!, this.extensionsWorkbenchService);
993
extensionsGridView.setExtensions(extensions);
994
scrollableContent.scanDomNode();
995
996
this.contentDisposables.add(scrollableContent);
997
this.contentDisposables.add(extensionsGridView);
998
this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout: () => scrollableContent.scanDomNode() })));
999
1000
return content;
1001
}
1002
1003
private loadContents<T>(loadingTask: () => CacheResult<T>, container: HTMLElement): Promise<T> {
1004
container.classList.add('loading');
1005
1006
const result = this.contentDisposables.add(loadingTask());
1007
const onDone = () => container.classList.remove('loading');
1008
result.promise.then(onDone, onDone);
1009
1010
return result.promise;
1011
}
1012
1013
layout(dimension: Dimension): void {
1014
this.dimension = dimension;
1015
this.layoutParticipants.forEach(p => p.layout());
1016
}
1017
1018
private onError(err: any): void {
1019
if (isCancellationError(err)) {
1020
return;
1021
}
1022
1023
this.notificationService.error(err);
1024
}
1025
}
1026
1027
class AdditionalDetailsWidget extends Disposable {
1028
1029
private readonly disposables = this._register(new DisposableStore());
1030
1031
constructor(
1032
private readonly container: HTMLElement,
1033
extension: IExtension,
1034
@IHoverService private readonly hoverService: IHoverService,
1035
@IOpenerService private readonly openerService: IOpenerService,
1036
@IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService,
1037
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
1038
@IFileService private readonly fileService: IFileService,
1039
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
1040
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
1041
@IExtensionGalleryManifestService private readonly extensionGalleryManifestService: IExtensionGalleryManifestService,
1042
) {
1043
super();
1044
this.render(extension);
1045
this._register(this.extensionsWorkbenchService.onChange(e => {
1046
if (e && areSameExtensions(e.identifier, extension.identifier) && e.server === extension.server) {
1047
this.render(e);
1048
}
1049
}));
1050
}
1051
1052
private render(extension: IExtension): void {
1053
this.container.innerText = '';
1054
this.disposables.clear();
1055
1056
if (extension.local) {
1057
this.renderInstallInfo(this.container, extension.local);
1058
}
1059
if (extension.gallery) {
1060
this.renderMarketplaceInfo(this.container, extension);
1061
}
1062
this.renderCategories(this.container, extension);
1063
this.renderExtensionResources(this.container, extension);
1064
}
1065
1066
private renderCategories(container: HTMLElement, extension: IExtension): void {
1067
if (extension.categories.length) {
1068
const categoriesContainer = append(container, $('.categories-container.additional-details-element'));
1069
append(categoriesContainer, $('.additional-details-title', undefined, localize('categories', "Categories")));
1070
const categoriesElement = append(categoriesContainer, $('.categories'));
1071
this.extensionGalleryManifestService.getExtensionGalleryManifest()
1072
.then(manifest => {
1073
const hasCategoryFilter = manifest?.capabilities.extensionQuery.filtering?.some(({ name }) => name === FilterType.Category);
1074
for (const category of extension.categories) {
1075
const categoryElement = append(categoriesElement, $('span.category', { tabindex: '0' }, category));
1076
if (hasCategoryFilter) {
1077
categoryElement.classList.add('clickable');
1078
this.disposables.add(onClick(categoryElement, () => this.extensionsWorkbenchService.openSearch(`@category:"${category}"`)));
1079
}
1080
}
1081
});
1082
}
1083
}
1084
1085
private renderExtensionResources(container: HTMLElement, extension: IExtension): void {
1086
const resources: [string, URI][] = [];
1087
if (extension.url) {
1088
resources.push([localize('Marketplace', "Marketplace"), URI.parse(extension.url)]);
1089
}
1090
if (extension.supportUrl) {
1091
try {
1092
resources.push([localize('issues', "Issues"), URI.parse(extension.supportUrl)]);
1093
} catch (error) {/* Ignore */ }
1094
}
1095
if (extension.repository) {
1096
try {
1097
resources.push([localize('repository', "Repository"), URI.parse(extension.repository)]);
1098
} catch (error) {/* Ignore */ }
1099
}
1100
if (extension.licenseUrl) {
1101
try {
1102
resources.push([localize('license', "License"), URI.parse(extension.licenseUrl)]);
1103
} catch (error) {/* Ignore */ }
1104
}
1105
if (extension.publisherUrl) {
1106
resources.push([extension.publisherDisplayName, extension.publisherUrl]);
1107
}
1108
if (resources.length || extension.publisherSponsorLink) {
1109
const extensionResourcesContainer = append(container, $('.resources-container.additional-details-element'));
1110
append(extensionResourcesContainer, $('.additional-details-title', undefined, localize('resources', "Resources")));
1111
const resourcesElement = append(extensionResourcesContainer, $('.resources'));
1112
for (const [label, uri] of resources) {
1113
const resource = append(resourcesElement, $('a.resource', { tabindex: '0' }, label));
1114
this.disposables.add(onClick(resource, () => this.openerService.open(uri)));
1115
this.disposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), resource, uri.toString()));
1116
}
1117
}
1118
}
1119
1120
private renderInstallInfo(container: HTMLElement, extension: ILocalExtension): void {
1121
const installInfoContainer = append(container, $('.more-info-container.additional-details-element'));
1122
append(installInfoContainer, $('.additional-details-title', undefined, localize('Install Info', "Installation")));
1123
const installInfo = append(installInfoContainer, $('.more-info'));
1124
append(installInfo,
1125
$('.more-info-entry', undefined,
1126
$('div.more-info-entry-name', undefined, localize('id', "Identifier")),
1127
$('code', undefined, extension.identifier.id)
1128
));
1129
if (extension.type !== ExtensionType.System) {
1130
append(installInfo,
1131
$('.more-info-entry', undefined,
1132
$('div.more-info-entry-name', undefined, localize('Version', "Version")),
1133
$('code', undefined, extension.manifest.version)
1134
)
1135
);
1136
}
1137
if (extension.installedTimestamp) {
1138
append(installInfo,
1139
$('.more-info-entry', undefined,
1140
$('div.more-info-entry-name', undefined, localize('last updated', "Last Updated")),
1141
$('div', undefined, toDateString(new Date(extension.installedTimestamp)))
1142
)
1143
);
1144
}
1145
if (!extension.isBuiltin && extension.source !== 'gallery') {
1146
const element = $('div', undefined, extension.source === 'vsix' ? localize('vsix', "VSIX") : localize('other', "Local"));
1147
append(installInfo,
1148
$('.more-info-entry', undefined,
1149
$('div.more-info-entry-name', undefined, localize('source', "Source")),
1150
element
1151
)
1152
);
1153
if (isNative && extension.source === 'resource' && extension.location.scheme === Schemas.file) {
1154
element.classList.add('link');
1155
element.title = extension.location.fsPath;
1156
this.disposables.add(onClick(element, () => this.openerService.open(extension.location, { openExternal: true })));
1157
}
1158
}
1159
if (extension.size) {
1160
const element = $('div', undefined, ByteSize.formatSize(extension.size));
1161
append(installInfo,
1162
$('.more-info-entry', undefined,
1163
$('div.more-info-entry-name', { title: localize('size when installed', "Size when installed") }, localize('size', "Size")),
1164
element
1165
)
1166
);
1167
if (isNative && extension.location.scheme === Schemas.file) {
1168
element.classList.add('link');
1169
element.title = extension.location.fsPath;
1170
this.disposables.add(onClick(element, () => this.openerService.open(extension.location, { openExternal: true })));
1171
}
1172
}
1173
this.getCacheLocation(extension).then(cacheLocation => {
1174
if (!cacheLocation) {
1175
return;
1176
}
1177
computeSize(cacheLocation, this.fileService).then(cacheSize => {
1178
if (!cacheSize) {
1179
return;
1180
}
1181
const element = $('div', undefined, ByteSize.formatSize(cacheSize));
1182
append(installInfo,
1183
$('.more-info-entry', undefined,
1184
$('div.more-info-entry-name', { title: localize('disk space used', "Cache size") }, localize('cache size', "Cache")),
1185
element)
1186
);
1187
if (isNative && extension.location.scheme === Schemas.file) {
1188
element.classList.add('link');
1189
element.title = cacheLocation.fsPath;
1190
this.disposables.add(onClick(element, () => this.openerService.open(cacheLocation.with({ scheme: Schemas.file }), { openExternal: true })));
1191
}
1192
});
1193
});
1194
}
1195
1196
private async getCacheLocation(extension: ILocalExtension): Promise<URI | undefined> {
1197
let extensionCacheLocation = this.uriIdentityService.extUri.joinPath(this.userDataProfilesService.defaultProfile.globalStorageHome, extension.identifier.id.toLowerCase());
1198
if (extension.location.scheme === Schemas.vscodeRemote) {
1199
const environment = await this.remoteAgentService.getEnvironment();
1200
if (!environment) {
1201
return undefined;
1202
}
1203
extensionCacheLocation = this.uriIdentityService.extUri.joinPath(environment.globalStorageHome, extension.identifier.id.toLowerCase());
1204
}
1205
return extensionCacheLocation;
1206
}
1207
1208
private renderMarketplaceInfo(container: HTMLElement, extension: IExtension): void {
1209
const gallery = extension.gallery;
1210
const moreInfoContainer = append(container, $('.more-info-container.additional-details-element'));
1211
append(moreInfoContainer, $('.additional-details-title', undefined, localize('Marketplace Info', "Marketplace")));
1212
const moreInfo = append(moreInfoContainer, $('.more-info'));
1213
if (gallery) {
1214
if (!extension.local) {
1215
append(moreInfo,
1216
$('.more-info-entry', undefined,
1217
$('div.more-info-entry-name', undefined, localize('id', "Identifier")),
1218
$('code', undefined, extension.identifier.id)
1219
));
1220
append(moreInfo,
1221
$('.more-info-entry', undefined,
1222
$('div.more-info-entry-name', undefined, localize('Version', "Version")),
1223
$('code', undefined, gallery.version)
1224
)
1225
);
1226
}
1227
append(moreInfo,
1228
$('.more-info-entry', undefined,
1229
$('div.more-info-entry-name', undefined, localize('published', "Published")),
1230
$('div', undefined, toDateString(new Date(gallery.releaseDate)))
1231
),
1232
$('.more-info-entry', undefined,
1233
$('div.more-info-entry-name', undefined, localize('last released', "Last Released")),
1234
$('div', undefined, toDateString(new Date(gallery.lastUpdated)))
1235
)
1236
);
1237
}
1238
}
1239
}
1240
1241
const contextKeyExpr = ContextKeyExpr.and(ContextKeyExpr.equals('activeEditor', ExtensionEditor.ID), EditorContextKeys.focus.toNegated());
1242
registerAction2(class ShowExtensionEditorFindAction extends Action2 {
1243
constructor() {
1244
super({
1245
id: 'editor.action.extensioneditor.showfind',
1246
title: localize('find', "Find"),
1247
keybinding: {
1248
when: contextKeyExpr,
1249
weight: KeybindingWeight.EditorContrib,
1250
primary: KeyMod.CtrlCmd | KeyCode.KeyF,
1251
}
1252
});
1253
}
1254
run(accessor: ServicesAccessor): void {
1255
const extensionEditor = getExtensionEditor(accessor);
1256
extensionEditor?.showFind();
1257
}
1258
});
1259
1260
registerAction2(class StartExtensionEditorFindNextAction extends Action2 {
1261
constructor() {
1262
super({
1263
id: 'editor.action.extensioneditor.findNext',
1264
title: localize('find next', "Find Next"),
1265
keybinding: {
1266
when: ContextKeyExpr.and(
1267
contextKeyExpr,
1268
KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED),
1269
primary: KeyCode.Enter,
1270
weight: KeybindingWeight.EditorContrib
1271
}
1272
});
1273
}
1274
run(accessor: ServicesAccessor): void {
1275
const extensionEditor = getExtensionEditor(accessor);
1276
extensionEditor?.runFindAction(false);
1277
}
1278
});
1279
1280
registerAction2(class StartExtensionEditorFindPreviousAction extends Action2 {
1281
constructor() {
1282
super({
1283
id: 'editor.action.extensioneditor.findPrevious',
1284
title: localize('find previous', "Find Previous"),
1285
keybinding: {
1286
when: ContextKeyExpr.and(
1287
contextKeyExpr,
1288
KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED),
1289
primary: KeyMod.Shift | KeyCode.Enter,
1290
weight: KeybindingWeight.EditorContrib
1291
}
1292
});
1293
}
1294
run(accessor: ServicesAccessor): void {
1295
const extensionEditor = getExtensionEditor(accessor);
1296
extensionEditor?.runFindAction(true);
1297
}
1298
});
1299
1300
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
1301
1302
const link = theme.getColor(textLinkForeground);
1303
if (link) {
1304
collector.addRule(`.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a.resource { color: ${link}; }`);
1305
collector.addRule(`.monaco-workbench .extension-editor .content .feature-contributions a { color: ${link}; }`);
1306
}
1307
1308
const activeLink = theme.getColor(textLinkActiveForeground);
1309
if (activeLink) {
1310
collector.addRule(`.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a.resource:hover,
1311
.monaco-workbench .extension-editor .content .details .additional-details-container .resources-container a.resource:active { color: ${activeLink}; }`);
1312
collector.addRule(`.monaco-workbench .extension-editor .content .feature-contributions a:hover,
1313
.monaco-workbench .extension-editor .content .feature-contributions a:active { color: ${activeLink}; }`);
1314
}
1315
1316
const buttonHoverBackgroundColor = theme.getColor(buttonHoverBackground);
1317
if (buttonHoverBackgroundColor) {
1318
collector.addRule(`.monaco-workbench .extension-editor .content > .details > .additional-details-container .categories-container > .categories > .category.clickable:hover { background-color: ${buttonHoverBackgroundColor}; border-color: ${buttonHoverBackgroundColor}; }`);
1319
}
1320
1321
const buttonForegroundColor = theme.getColor(buttonForeground);
1322
if (buttonForegroundColor) {
1323
collector.addRule(`.monaco-workbench .extension-editor .content > .details > .additional-details-container .categories-container > .categories > .category.clickable:hover { color: ${buttonForegroundColor}; }`);
1324
}
1325
1326
});
1327
1328
function getExtensionEditor(accessor: ServicesAccessor): ExtensionEditor | null {
1329
const activeEditorPane = accessor.get(IEditorService).activeEditorPane;
1330
if (activeEditorPane instanceof ExtensionEditor) {
1331
return activeEditorPane;
1332
}
1333
return null;
1334
}
1335
1336