Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionsViewer.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 * as dom from '../../../../base/browser/dom.js';
7
import { localize } from '../../../../nls.js';
8
import { IDisposable, dispose, Disposable, DisposableStore, toDisposable, isDisposable } from '../../../../base/common/lifecycle.js';
9
import { Action, ActionRunner, IAction, Separator } from '../../../../base/common/actions.js';
10
import { IExtensionsWorkbenchService, IExtension, IExtensionsViewState } from '../common/extensions.js';
11
import { Event } from '../../../../base/common/event.js';
12
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
13
import { IListService, IWorkbenchPagedListOptions, WorkbenchAsyncDataTree, WorkbenchPagedList } from '../../../../platform/list/browser/listService.js';
14
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
15
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
16
import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from '../../../../platform/theme/common/themeService.js';
17
import { IAsyncDataSource, ITreeNode } from '../../../../base/browser/ui/tree/tree.js';
18
import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js';
19
import { CancellationToken } from '../../../../base/common/cancellation.js';
20
import { isNonEmptyArray } from '../../../../base/common/arrays.js';
21
import { Delegate, Renderer } from './extensionsList.js';
22
import { listFocusForeground, listFocusBackground, foreground, editorBackground } from '../../../../platform/theme/common/colorRegistry.js';
23
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
24
import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
25
import { KeyCode } from '../../../../base/common/keyCodes.js';
26
import { IListStyles } from '../../../../base/browser/ui/list/listWidget.js';
27
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
28
import { IStyleOverride } from '../../../../platform/theme/browser/defaultStyles.js';
29
import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js';
30
import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js';
31
import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js';
32
import { ExtensionAction, getContextMenuActions, ManageExtensionAction } from './extensionsActions.js';
33
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
34
import { INotificationService } from '../../../../platform/notification/common/notification.js';
35
import { getLocationBasedViewColors } from '../../../browser/parts/views/viewPane.js';
36
import { DelayedPagedModel, IPagedModel } from '../../../../base/common/paging.js';
37
import { ExtensionIconWidget } from './extensionsWidgets.js';
38
39
function getAriaLabelForExtension(extension: IExtension | null): string {
40
if (!extension) {
41
return '';
42
}
43
const publisher = extension.publisherDomain?.verified ? localize('extension.arialabel.verifiedPublisher', "Verified Publisher {0}", extension.publisherDisplayName) : localize('extension.arialabel.publisher', "Publisher {0}", extension.publisherDisplayName);
44
const deprecated = extension?.deprecationInfo ? localize('extension.arialabel.deprecated', "Deprecated") : '';
45
const rating = extension?.rating ? localize('extension.arialabel.rating', "Rated {0} out of 5 stars by {1} users", extension.rating.toFixed(2), extension.ratingCount) : '';
46
return `${extension.displayName}, ${deprecated ? `${deprecated}, ` : ''}${extension.version}, ${publisher}, ${extension.description} ${rating ? `, ${rating}` : ''}`;
47
}
48
49
export class ExtensionsList extends Disposable {
50
51
readonly list: WorkbenchPagedList<IExtension>;
52
private readonly contextMenuActionRunner = this._register(new ActionRunner());
53
54
constructor(
55
parent: HTMLElement,
56
viewId: string,
57
options: Partial<IWorkbenchPagedListOptions<IExtension>>,
58
extensionsViewState: IExtensionsViewState,
59
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
60
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
61
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
62
@INotificationService notificationService: INotificationService,
63
@IContextMenuService private readonly contextMenuService: IContextMenuService,
64
@IContextKeyService private readonly contextKeyService: IContextKeyService,
65
@IInstantiationService private readonly instantiationService: IInstantiationService,
66
) {
67
super();
68
this._register(this.contextMenuActionRunner.onDidRun(({ error }) => error && notificationService.error(error)));
69
const delegate = new Delegate();
70
const renderer = instantiationService.createInstance(Renderer, extensionsViewState, {
71
hoverOptions: {
72
position: () => {
73
const viewLocation = viewDescriptorService.getViewLocationById(viewId);
74
if (viewLocation === ViewContainerLocation.Sidebar) {
75
return layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT;
76
}
77
if (viewLocation === ViewContainerLocation.AuxiliaryBar) {
78
return layoutService.getSideBarPosition() === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT;
79
}
80
return HoverPosition.RIGHT;
81
}
82
}
83
});
84
this.list = instantiationService.createInstance(WorkbenchPagedList, `${viewId}-Extensions`, parent, delegate, [renderer], {
85
multipleSelectionSupport: false,
86
setRowLineHeight: false,
87
horizontalScrolling: false,
88
accessibilityProvider: {
89
getAriaLabel(extension: IExtension | null): string {
90
return getAriaLabelForExtension(extension);
91
},
92
getWidgetAriaLabel(): string {
93
return localize('extensions', "Extensions");
94
}
95
},
96
overrideStyles: getLocationBasedViewColors(viewDescriptorService.getViewLocationById(viewId)).listOverrideStyles,
97
openOnSingleClick: true,
98
...options
99
}) as WorkbenchPagedList<IExtension>;
100
this._register(this.list.onContextMenu(e => this.onContextMenu(e), this));
101
this._register(this.list);
102
103
this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => {
104
this.openExtension(options.element!, { sideByside: options.sideBySide, ...options.editorOptions });
105
}));
106
}
107
108
setModel(model: IPagedModel<IExtension>) {
109
this.list.model = new DelayedPagedModel(model);
110
}
111
112
layout(height?: number, width?: number): void {
113
this.list.layout(height, width);
114
}
115
116
private openExtension(extension: IExtension, options: { sideByside?: boolean; preserveFocus?: boolean; pinned?: boolean }): void {
117
extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0] || extension;
118
this.extensionsWorkbenchService.open(extension, options);
119
}
120
121
private async onContextMenu(e: IListContextMenuEvent<IExtension>): Promise<void> {
122
if (e.element) {
123
const disposables = new DisposableStore();
124
const manageExtensionAction = disposables.add(this.instantiationService.createInstance(ManageExtensionAction));
125
const extension = e.element ? this.extensionsWorkbenchService.local.find(local => areSameExtensions(local.identifier, e.element!.identifier) && (!e.element!.server || e.element!.server === local.server)) || e.element
126
: e.element;
127
manageExtensionAction.extension = extension;
128
let groups: IAction[][] = [];
129
if (manageExtensionAction.enabled) {
130
groups = await manageExtensionAction.getActionGroups();
131
} else if (extension) {
132
groups = await getContextMenuActions(extension, this.contextKeyService, this.instantiationService);
133
groups.forEach(group => group.forEach(extensionAction => {
134
if (extensionAction instanceof ExtensionAction) {
135
extensionAction.extension = extension;
136
}
137
}));
138
}
139
const actions: IAction[] = [];
140
for (const menuActions of groups) {
141
for (const menuAction of menuActions) {
142
actions.push(menuAction);
143
if (isDisposable(menuAction)) {
144
disposables.add(menuAction);
145
}
146
}
147
actions.push(new Separator());
148
}
149
actions.pop();
150
this.contextMenuService.showContextMenu({
151
getAnchor: () => e.anchor,
152
getActions: () => actions,
153
actionRunner: this.contextMenuActionRunner,
154
onHide: () => disposables.dispose()
155
});
156
}
157
}
158
}
159
160
export class ExtensionsGridView extends Disposable {
161
162
readonly element: HTMLElement;
163
private readonly renderer: Renderer;
164
private readonly delegate: Delegate;
165
private readonly disposableStore: DisposableStore;
166
167
constructor(
168
parent: HTMLElement,
169
delegate: Delegate,
170
@IInstantiationService private readonly instantiationService: IInstantiationService
171
) {
172
super();
173
this.element = dom.append(parent, dom.$('.extensions-grid-view'));
174
this.renderer = this.instantiationService.createInstance(Renderer, { onFocus: Event.None, onBlur: Event.None, filters: {} }, { hoverOptions: { position() { return HoverPosition.BELOW; } } });
175
this.delegate = delegate;
176
this.disposableStore = this._register(new DisposableStore());
177
}
178
179
setExtensions(extensions: IExtension[]): void {
180
this.disposableStore.clear();
181
extensions.forEach((e, index) => this.renderExtension(e, index));
182
}
183
184
private renderExtension(extension: IExtension, index: number): void {
185
const extensionContainer = dom.append(this.element, dom.$('.extension-container'));
186
extensionContainer.style.height = `${this.delegate.getHeight()}px`;
187
extensionContainer.setAttribute('tabindex', '0');
188
189
const template = this.renderer.renderTemplate(extensionContainer);
190
this.disposableStore.add(toDisposable(() => this.renderer.disposeTemplate(template)));
191
192
const openExtensionAction = this.instantiationService.createInstance(OpenExtensionAction);
193
openExtensionAction.extension = extension;
194
template.name.setAttribute('tabindex', '0');
195
196
const handleEvent = (e: StandardMouseEvent | StandardKeyboardEvent) => {
197
if (e instanceof StandardKeyboardEvent && e.keyCode !== KeyCode.Enter) {
198
return;
199
}
200
openExtensionAction.run(e.ctrlKey || e.metaKey);
201
e.stopPropagation();
202
e.preventDefault();
203
};
204
205
this.disposableStore.add(dom.addDisposableListener(template.name, dom.EventType.CLICK, (e: MouseEvent) => handleEvent(new StandardMouseEvent(dom.getWindow(template.name), e))));
206
this.disposableStore.add(dom.addDisposableListener(template.name, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => handleEvent(new StandardKeyboardEvent(e))));
207
this.disposableStore.add(dom.addDisposableListener(extensionContainer, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => handleEvent(new StandardKeyboardEvent(e))));
208
209
this.renderer.renderElement(extension, index, template);
210
}
211
}
212
213
interface IExtensionTemplateData {
214
name: HTMLElement;
215
identifier: HTMLElement;
216
author: HTMLElement;
217
extensionDisposables: IDisposable[];
218
extensionData: IExtensionData;
219
}
220
221
interface IUnknownExtensionTemplateData {
222
identifier: HTMLElement;
223
}
224
225
interface IExtensionData {
226
extension: IExtension;
227
hasChildren: boolean;
228
getChildren: () => Promise<IExtensionData[] | null>;
229
parent: IExtensionData | null;
230
}
231
232
class AsyncDataSource implements IAsyncDataSource<IExtensionData, any> {
233
234
public hasChildren({ hasChildren }: IExtensionData): boolean {
235
return hasChildren;
236
}
237
238
public getChildren(extensionData: IExtensionData): Promise<any> {
239
return extensionData.getChildren();
240
}
241
242
}
243
244
class VirualDelegate implements IListVirtualDelegate<IExtensionData> {
245
246
public getHeight(element: IExtensionData): number {
247
return 62;
248
}
249
public getTemplateId({ extension }: IExtensionData): string {
250
return extension ? ExtensionRenderer.TEMPLATE_ID : UnknownExtensionRenderer.TEMPLATE_ID;
251
}
252
}
253
254
class ExtensionRenderer implements IListRenderer<ITreeNode<IExtensionData>, IExtensionTemplateData> {
255
256
static readonly TEMPLATE_ID = 'extension-template';
257
258
constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) {
259
}
260
261
public get templateId(): string {
262
return ExtensionRenderer.TEMPLATE_ID;
263
}
264
265
public renderTemplate(container: HTMLElement): IExtensionTemplateData {
266
container.classList.add('extension');
267
268
const iconWidget = this.instantiationService.createInstance(ExtensionIconWidget, container);
269
const details = dom.append(container, dom.$('.details'));
270
271
const header = dom.append(details, dom.$('.header'));
272
const name = dom.append(header, dom.$('span.name'));
273
const openExtensionAction = this.instantiationService.createInstance(OpenExtensionAction);
274
const extensionDisposables = [dom.addDisposableListener(name, 'click', (e: MouseEvent) => {
275
openExtensionAction.run(e.ctrlKey || e.metaKey);
276
e.stopPropagation();
277
e.preventDefault();
278
}), iconWidget, openExtensionAction];
279
const identifier = dom.append(header, dom.$('span.identifier'));
280
281
const footer = dom.append(details, dom.$('.footer'));
282
const author = dom.append(footer, dom.$('.author'));
283
return {
284
name,
285
identifier,
286
author,
287
extensionDisposables,
288
set extensionData(extensionData: IExtensionData) {
289
iconWidget.extension = extensionData.extension;
290
openExtensionAction.extension = extensionData.extension;
291
}
292
};
293
}
294
295
public renderElement(node: ITreeNode<IExtensionData>, index: number, data: IExtensionTemplateData): void {
296
const extension = node.element.extension;
297
data.name.textContent = extension.displayName;
298
data.identifier.textContent = extension.identifier.id;
299
data.author.textContent = extension.publisherDisplayName;
300
data.extensionData = node.element;
301
}
302
303
public disposeTemplate(templateData: IExtensionTemplateData): void {
304
templateData.extensionDisposables = dispose((<IExtensionTemplateData>templateData).extensionDisposables);
305
}
306
}
307
308
class UnknownExtensionRenderer implements IListRenderer<ITreeNode<IExtensionData>, IUnknownExtensionTemplateData> {
309
310
static readonly TEMPLATE_ID = 'unknown-extension-template';
311
312
public get templateId(): string {
313
return UnknownExtensionRenderer.TEMPLATE_ID;
314
}
315
316
public renderTemplate(container: HTMLElement): IUnknownExtensionTemplateData {
317
const messageContainer = dom.append(container, dom.$('div.unknown-extension'));
318
dom.append(messageContainer, dom.$('span.error-marker')).textContent = localize('error', "Error");
319
dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Extension', "Unknown Extension:");
320
321
const identifier = dom.append(messageContainer, dom.$('span.message'));
322
return { identifier };
323
}
324
325
public renderElement(node: ITreeNode<IExtensionData>, index: number, data: IUnknownExtensionTemplateData): void {
326
data.identifier.textContent = node.element.extension.identifier.id;
327
}
328
329
public disposeTemplate(data: IUnknownExtensionTemplateData): void {
330
}
331
}
332
333
class OpenExtensionAction extends Action {
334
335
private _extension: IExtension | undefined;
336
337
constructor(@IExtensionsWorkbenchService private readonly extensionsWorkdbenchService: IExtensionsWorkbenchService) {
338
super('extensions.action.openExtension', '');
339
}
340
341
public set extension(extension: IExtension) {
342
this._extension = extension;
343
}
344
345
override run(sideByside: boolean): Promise<any> {
346
if (this._extension) {
347
return this.extensionsWorkdbenchService.open(this._extension, { sideByside });
348
}
349
return Promise.resolve();
350
}
351
}
352
353
export class ExtensionsTree extends WorkbenchAsyncDataTree<IExtensionData, IExtensionData> {
354
355
constructor(
356
input: IExtensionData,
357
container: HTMLElement,
358
overrideStyles: IStyleOverride<IListStyles>,
359
@IContextKeyService contextKeyService: IContextKeyService,
360
@IListService listService: IListService,
361
@IInstantiationService instantiationService: IInstantiationService,
362
@IConfigurationService configurationService: IConfigurationService,
363
@IExtensionsWorkbenchService extensionsWorkdbenchService: IExtensionsWorkbenchService
364
) {
365
const delegate = new VirualDelegate();
366
const dataSource = new AsyncDataSource();
367
const renderers = [instantiationService.createInstance(ExtensionRenderer), instantiationService.createInstance(UnknownExtensionRenderer)];
368
const identityProvider = {
369
getId({ extension, parent }: IExtensionData): string {
370
return parent ? this.getId(parent) + '/' + extension.identifier.id : extension.identifier.id;
371
}
372
};
373
374
super(
375
'ExtensionsTree',
376
container,
377
delegate,
378
renderers,
379
dataSource,
380
{
381
indent: 40,
382
identityProvider,
383
multipleSelectionSupport: false,
384
overrideStyles,
385
accessibilityProvider: {
386
getAriaLabel(extensionData: IExtensionData): string {
387
return getAriaLabelForExtension(extensionData.extension);
388
},
389
getWidgetAriaLabel(): string {
390
return localize('extensions', "Extensions");
391
}
392
}
393
},
394
instantiationService, contextKeyService, listService, configurationService
395
);
396
397
this.setInput(input);
398
399
this.disposables.add(this.onDidChangeSelection(event => {
400
if (dom.isKeyboardEvent(event.browserEvent)) {
401
extensionsWorkdbenchService.open(event.elements[0].extension, { sideByside: false });
402
}
403
}));
404
}
405
}
406
407
export class ExtensionData implements IExtensionData {
408
409
readonly extension: IExtension;
410
readonly parent: IExtensionData | null;
411
private readonly getChildrenExtensionIds: (extension: IExtension) => string[];
412
private readonly childrenExtensionIds: string[];
413
private readonly extensionsWorkbenchService: IExtensionsWorkbenchService;
414
415
constructor(extension: IExtension, parent: IExtensionData | null, getChildrenExtensionIds: (extension: IExtension) => string[], extensionsWorkbenchService: IExtensionsWorkbenchService) {
416
this.extension = extension;
417
this.parent = parent;
418
this.getChildrenExtensionIds = getChildrenExtensionIds;
419
this.extensionsWorkbenchService = extensionsWorkbenchService;
420
this.childrenExtensionIds = this.getChildrenExtensionIds(extension);
421
}
422
423
get hasChildren(): boolean {
424
return isNonEmptyArray(this.childrenExtensionIds);
425
}
426
427
async getChildren(): Promise<IExtensionData[] | null> {
428
if (this.hasChildren) {
429
const result: IExtension[] = await getExtensions(this.childrenExtensionIds, this.extensionsWorkbenchService);
430
return result.map(extension => new ExtensionData(extension, this, this.getChildrenExtensionIds, this.extensionsWorkbenchService));
431
}
432
return null;
433
}
434
}
435
436
export async function getExtensions(extensions: string[], extensionsWorkbenchService: IExtensionsWorkbenchService): Promise<IExtension[]> {
437
const localById = extensionsWorkbenchService.local.reduce((result, e) => { result.set(e.identifier.id.toLowerCase(), e); return result; }, new Map<string, IExtension>());
438
const result: IExtension[] = [];
439
const toQuery: string[] = [];
440
for (const extensionId of extensions) {
441
const id = extensionId.toLowerCase();
442
const local = localById.get(id);
443
if (local) {
444
result.push(local);
445
} else {
446
toQuery.push(id);
447
}
448
}
449
if (toQuery.length) {
450
const galleryResult = await extensionsWorkbenchService.getExtensions(toQuery.map(id => ({ id })), CancellationToken.None);
451
result.push(...galleryResult);
452
}
453
return result;
454
}
455
456
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
457
const focusBackground = theme.getColor(listFocusBackground);
458
if (focusBackground) {
459
collector.addRule(`.extensions-grid-view .extension-container:focus { background-color: ${focusBackground}; outline: none; }`);
460
}
461
const focusForeground = theme.getColor(listFocusForeground);
462
if (focusForeground) {
463
collector.addRule(`.extensions-grid-view .extension-container:focus { color: ${focusForeground}; }`);
464
}
465
const foregroundColor = theme.getColor(foreground);
466
const editorBackgroundColor = theme.getColor(editorBackground);
467
if (foregroundColor && editorBackgroundColor) {
468
const authorForeground = foregroundColor.transparent(.9).makeOpaque(editorBackgroundColor);
469
collector.addRule(`.extensions-grid-view .extension-container:not(.disabled) .author { color: ${authorForeground}; }`);
470
const disabledExtensionForeground = foregroundColor.transparent(.5).makeOpaque(editorBackgroundColor);
471
collector.addRule(`.extensions-grid-view .extension-container.disabled { color: ${disabledExtensionForeground}; }`);
472
}
473
});
474
475