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