Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts
5241 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './media/extensionsViewlet.css';
7
import { localize, localize2 } from '../../../../nls.js';
8
import { timeout, Delayer } from '../../../../base/common/async.js';
9
import { isCancellationError } from '../../../../base/common/errors.js';
10
import { createErrorWithActions } from '../../../../base/common/errorMessage.js';
11
import { IWorkbenchContribution } from '../../../common/contributions.js';
12
import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';
13
import { Event } from '../../../../base/common/event.js';
14
import { Action } from '../../../../base/common/actions.js';
15
import { append, $, Dimension, hide, show, DragAndDropObserver, trackFocus, addDisposableListener, EventType, clearNode } from '../../../../base/browser/dom.js';
16
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
17
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
18
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
19
import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, CloseExtensionDetailsOnViewChangeKey, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, AutoCheckUpdatesConfigurationKey, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, AutoRestartConfigurationKey, ExtensionRuntimeActionType, SearchMcpServersContext, DefaultViewsContext, CONTEXT_EXTENSIONS_GALLERY_STATUS } from '../common/extensions.js';
20
import { InstallLocalExtensionsInRemoteAction, InstallRemoteExtensionsInLocalAction } from './extensionsActions.js';
21
import { IExtensionManagementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js';
22
import { IWorkbenchExtensionEnablementService, IExtensionManagementServerService, IExtensionManagementServer } from '../../../services/extensionManagement/common/extensionManagement.js';
23
import { ExtensionsInput } from '../common/extensionsInput.js';
24
import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, RecommendedExtensionsView, WorkspaceRecommendedExtensionsView, ServerInstalledExtensionsView, DefaultRecommendedExtensionsView, UntrustedWorkspaceUnsupportedExtensionsView, UntrustedWorkspacePartiallySupportedExtensionsView, VirtualWorkspaceUnsupportedExtensionsView, VirtualWorkspacePartiallySupportedExtensionsView, DefaultPopularExtensionsView, DeprecatedExtensionsView, SearchMarketplaceExtensionsView, RecentlyUpdatedExtensionsView, OutdatedExtensionsView, StaticQueryExtensionsView, NONE_CATEGORY, AbstractExtensionsListView } from './extensionsViews.js';
25
import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';
26
import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
27
import Severity from '../../../../base/common/severity.js';
28
import { IActivityService, IBadge, NumberBadge, WarningBadge } from '../../../services/activity/common/activity.js';
29
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
30
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
31
import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewDescriptorService, IAddedViewDescriptorRef, ViewContainerLocation } from '../../../common/views.js';
32
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
33
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
34
import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from '../../../../platform/contextkey/common/contextkey.js';
35
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
36
import { ILogService } from '../../../../platform/log/common/log.js';
37
import { INotificationService, IPromptChoice, NotificationPriority } from '../../../../platform/notification/common/notification.js';
38
import { IHostService } from '../../../services/host/browser/host.js';
39
import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js';
40
import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js';
41
import { ViewPane } from '../../../browser/parts/views/viewPane.js';
42
import { Query } from '../common/extensionQuery.js';
43
import { SuggestEnabledInput } from '../../codeEditor/browser/suggestEnabledInput/suggestEnabledInput.js';
44
import { alert } from '../../../../base/browser/ui/aria/aria.js';
45
import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js';
46
import { Registry } from '../../../../platform/registry/common/platform.js';
47
import { ILabelService } from '../../../../platform/label/common/label.js';
48
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
49
import { IPreferencesService } from '../../../services/preferences/common/preferences.js';
50
import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from '../../../common/theme.js';
51
import { VirtualWorkspaceContext, WorkbenchStateContext } from '../../../common/contextkeys.js';
52
import { ICommandService } from '../../../../platform/commands/common/commands.js';
53
import { installLocalInRemoteIcon } from './extensionsIcons.js';
54
import { registerAction2, Action2, MenuId } from '../../../../platform/actions/common/actions.js';
55
import { IPaneComposite } from '../../../common/panecomposite.js';
56
import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js';
57
import { coalesce } from '../../../../base/common/arrays.js';
58
import { extractEditorsAndFilesDropData } from '../../../../platform/dnd/browser/dnd.js';
59
import { extname } from '../../../../base/common/resources.js';
60
import { ILocalizedString } from '../../../../platform/action/common/action.js';
61
import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js';
62
import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
63
import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
64
import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js';
65
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
66
import { KeyCode } from '../../../../base/common/keyCodes.js';
67
import { ThemeIcon } from '../../../../base/common/themables.js';
68
import { Codicon } from '../../../../base/common/codicons.js';
69
import { IExtensionGalleryManifest, IExtensionGalleryManifestService, ExtensionGalleryManifestStatus } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
70
import { URI } from '../../../../base/common/uri.js';
71
import { DEFAULT_ACCOUNT_SIGN_IN_COMMAND } from '../../../services/accounts/browser/defaultAccount.js';
72
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
73
74
export const ExtensionsSortByContext = new RawContextKey<string>('extensionsSortByValue', '');
75
export const SearchMarketplaceExtensionsContext = new RawContextKey<boolean>('searchMarketplaceExtensions', false);
76
export const SearchHasTextContext = new RawContextKey<boolean>('extensionSearchHasText', false);
77
const InstalledExtensionsContext = new RawContextKey<boolean>('installedExtensions', false);
78
const SearchInstalledExtensionsContext = new RawContextKey<boolean>('searchInstalledExtensions', false);
79
const SearchRecentlyUpdatedExtensionsContext = new RawContextKey<boolean>('searchRecentlyUpdatedExtensions', false);
80
const SearchExtensionUpdatesContext = new RawContextKey<boolean>('searchExtensionUpdates', false);
81
const SearchOutdatedExtensionsContext = new RawContextKey<boolean>('searchOutdatedExtensions', false);
82
const SearchEnabledExtensionsContext = new RawContextKey<boolean>('searchEnabledExtensions', false);
83
const SearchDisabledExtensionsContext = new RawContextKey<boolean>('searchDisabledExtensions', false);
84
const HasInstalledExtensionsContext = new RawContextKey<boolean>('hasInstalledExtensions', true);
85
export const BuiltInExtensionsContext = new RawContextKey<boolean>('builtInExtensions', false);
86
const SearchBuiltInExtensionsContext = new RawContextKey<boolean>('searchBuiltInExtensions', false);
87
const SearchUnsupportedWorkspaceExtensionsContext = new RawContextKey<boolean>('searchUnsupportedWorkspaceExtensions', false);
88
const SearchDeprecatedExtensionsContext = new RawContextKey<boolean>('searchDeprecatedExtensions', false);
89
export const RecommendedExtensionsContext = new RawContextKey<boolean>('recommendedExtensions', false);
90
const SortByUpdateDateContext = new RawContextKey<boolean>('sortByUpdateDate', false);
91
export const ExtensionsSearchValueContext = new RawContextKey<string>('extensionsSearchValue', '');
92
93
const REMOTE_CATEGORY: ILocalizedString = localize2({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote");
94
95
interface IExtensionsViewletState {
96
'query.value'?: string;
97
}
98
99
export class ExtensionsViewletViewsContribution extends Disposable implements IWorkbenchContribution {
100
101
private readonly container: ViewContainer;
102
103
constructor(
104
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
105
@ILabelService private readonly labelService: ILabelService,
106
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
107
@IContextKeyService private readonly contextKeyService: IContextKeyService
108
) {
109
super();
110
111
this.container = viewDescriptorService.getViewContainerById(VIEWLET_ID)!;
112
this.registerViews();
113
}
114
115
private registerViews(): void {
116
const viewDescriptors: IViewDescriptor[] = [];
117
118
/* Default views */
119
viewDescriptors.push(...this.createDefaultExtensionsViewDescriptors());
120
121
/* Search views */
122
viewDescriptors.push(...this.createSearchExtensionsViewDescriptors());
123
124
/* Recommendations views */
125
viewDescriptors.push(...this.createRecommendedExtensionsViewDescriptors());
126
127
/* Built-in extensions views */
128
viewDescriptors.push(...this.createBuiltinExtensionsViewDescriptors());
129
130
/* Trust Required extensions views */
131
viewDescriptors.push(...this.createUnsupportedWorkspaceExtensionsViewDescriptors());
132
133
/* Other Local Filtered extensions views */
134
viewDescriptors.push(...this.createOtherLocalFilteredExtensionsViewDescriptors());
135
136
137
viewDescriptors.push({
138
id: 'workbench.views.extensions.marketplaceAccess',
139
name: localize2('marketPlace', "Marketplace"),
140
ctorDescriptor: new SyncDescriptor(class extends ViewPane {
141
public override shouldShowWelcome() {
142
return true;
143
}
144
}),
145
when: ContextKeyExpr.and(
146
ContextKeyExpr.or(
147
ContextKeyExpr.has('searchMarketplaceExtensions'), ContextKeyExpr.and(DefaultViewsContext)
148
),
149
ContextKeyExpr.or(CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.RequiresSignIn), CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.AccessDenied))
150
),
151
order: -1,
152
});
153
154
const viewRegistry = Registry.as<IViewsRegistry>(Extensions.ViewsRegistry);
155
viewRegistry.registerViews(viewDescriptors, this.container);
156
157
viewRegistry.registerViewWelcomeContent('workbench.views.extensions.marketplaceAccess', {
158
content: localize('sign in', "[Sign in to access Extensions Marketplace]({0})", `command:${DEFAULT_ACCOUNT_SIGN_IN_COMMAND}`),
159
when: CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.RequiresSignIn)
160
});
161
162
viewRegistry.registerViewWelcomeContent('workbench.views.extensions.marketplaceAccess', {
163
content: localize('access denied', "Your account does not have access to the Extensions Marketplace. Please contact your administrator."),
164
when: CONTEXT_EXTENSIONS_GALLERY_STATUS.isEqualTo(ExtensionGalleryManifestStatus.AccessDenied)
165
});
166
}
167
168
private createDefaultExtensionsViewDescriptors(): IViewDescriptor[] {
169
const viewDescriptors: IViewDescriptor[] = [];
170
171
/*
172
* Default installed extensions views - Shows all user installed extensions.
173
*/
174
const servers: IExtensionManagementServer[] = [];
175
if (this.extensionManagementServerService.localExtensionManagementServer) {
176
servers.push(this.extensionManagementServerService.localExtensionManagementServer);
177
}
178
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
179
servers.push(this.extensionManagementServerService.remoteExtensionManagementServer);
180
}
181
if (this.extensionManagementServerService.webExtensionManagementServer) {
182
servers.push(this.extensionManagementServerService.webExtensionManagementServer);
183
}
184
const getViewName = (viewTitle: string, server: IExtensionManagementServer): string => {
185
return servers.length > 1 ? `${server.label} - ${viewTitle}` : viewTitle;
186
};
187
let installedWebExtensionsContextChangeEvent = Event.None;
188
if (this.extensionManagementServerService.webExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) {
189
const interestingContextKeys = new Set();
190
interestingContextKeys.add('hasInstalledWebExtensions');
191
installedWebExtensionsContextChangeEvent = Event.filter(this.contextKeyService.onDidChangeContext, e => e.affectsSome(interestingContextKeys));
192
}
193
const serverLabelChangeEvent = Event.any(this.labelService.onDidChangeFormatters, installedWebExtensionsContextChangeEvent);
194
for (const server of servers) {
195
const getInstalledViewName = (): string => getViewName(localize('installed', "Installed"), server);
196
const onDidChangeTitle = Event.map<void, string>(serverLabelChangeEvent, () => getInstalledViewName());
197
const id = servers.length > 1 ? `workbench.views.extensions.${server.id}.installed` : `workbench.views.extensions.installed`;
198
/* Installed extensions view */
199
viewDescriptors.push({
200
id,
201
get name() {
202
return {
203
value: getInstalledViewName(),
204
original: getViewName('Installed', server)
205
};
206
},
207
weight: 100,
208
order: 1,
209
when: ContextKeyExpr.and(DefaultViewsContext),
210
ctorDescriptor: new SyncDescriptor(ServerInstalledExtensionsView, [{ server, flexibleHeight: true, onDidChangeTitle }]),
211
/* Installed extensions views shall not be allowed to hidden when there are more than one server */
212
canToggleVisibility: servers.length === 1
213
});
214
215
if (server === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer) {
216
this._register(registerAction2(class InstallLocalExtensionsInRemoteAction2 extends Action2 {
217
constructor() {
218
super({
219
id: 'workbench.extensions.installLocalExtensions',
220
get title() {
221
return localize2('select and install local extensions', "Install Local Extensions in '{0}'...", server.label);
222
},
223
category: REMOTE_CATEGORY,
224
icon: installLocalInRemoteIcon,
225
f1: true,
226
menu: {
227
id: MenuId.ViewTitle,
228
when: ContextKeyExpr.equals('view', id),
229
group: 'navigation',
230
}
231
});
232
}
233
run(accessor: ServicesAccessor): Promise<void> {
234
return accessor.get(IInstantiationService).createInstance(InstallLocalExtensionsInRemoteAction).run();
235
}
236
}));
237
}
238
}
239
240
if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) {
241
this._register(registerAction2(class InstallRemoteExtensionsInLocalAction2 extends Action2 {
242
constructor() {
243
super({
244
id: 'workbench.extensions.actions.installLocalExtensionsInRemote',
245
title: localize2('install remote in local', 'Install Remote Extensions Locally...'),
246
category: REMOTE_CATEGORY,
247
f1: true
248
});
249
}
250
run(accessor: ServicesAccessor): Promise<void> {
251
return accessor.get(IInstantiationService).createInstance(InstallRemoteExtensionsInLocalAction, 'workbench.extensions.actions.installLocalExtensionsInRemote').run();
252
}
253
}));
254
}
255
256
/*
257
* Default popular extensions view
258
* Separate view for popular extensions required as we need to show popular and recommended sections
259
* in the default view when there is no search text, and user has no installed extensions.
260
*/
261
viewDescriptors.push({
262
id: 'workbench.views.extensions.popular',
263
name: localize2('popularExtensions', "Popular"),
264
ctorDescriptor: new SyncDescriptor(DefaultPopularExtensionsView, [{ hideBadge: true }]),
265
when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.not('hasInstalledExtensions'), CONTEXT_HAS_GALLERY),
266
weight: 60,
267
order: 2,
268
canToggleVisibility: false
269
});
270
271
/*
272
* Default recommended extensions view
273
* When user has installed extensions, this is shown along with the views for enabled & disabled extensions
274
* When user has no installed extensions, this is shown along with the view for popular extensions
275
*/
276
viewDescriptors.push({
277
id: 'extensions.recommendedList',
278
name: localize2('recommendedExtensions', "Recommended"),
279
ctorDescriptor: new SyncDescriptor(DefaultRecommendedExtensionsView, [{ flexibleHeight: true }]),
280
when: ContextKeyExpr.and(DefaultViewsContext, SortByUpdateDateContext.negate(), ContextKeyExpr.not('config.extensions.showRecommendationsOnlyOnDemand'), CONTEXT_HAS_GALLERY),
281
weight: 40,
282
order: 3,
283
canToggleVisibility: true
284
});
285
286
/* Installed views shall be default in multi server window */
287
if (servers.length === 1) {
288
/*
289
* Default enabled extensions view - Shows all user installed enabled extensions.
290
* Hidden by default
291
*/
292
viewDescriptors.push({
293
id: 'workbench.views.extensions.enabled',
294
name: localize2('enabledExtensions', "Enabled"),
295
ctorDescriptor: new SyncDescriptor(EnabledExtensionsView, [{}]),
296
when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.has('hasInstalledExtensions')),
297
hideByDefault: true,
298
weight: 40,
299
order: 4,
300
canToggleVisibility: true
301
});
302
303
/*
304
* Default disabled extensions view - Shows all disabled extensions.
305
* Hidden by default
306
*/
307
viewDescriptors.push({
308
id: 'workbench.views.extensions.disabled',
309
name: localize2('disabledExtensions', "Disabled"),
310
ctorDescriptor: new SyncDescriptor(DisabledExtensionsView, [{}]),
311
when: ContextKeyExpr.and(DefaultViewsContext, ContextKeyExpr.has('hasInstalledExtensions')),
312
hideByDefault: true,
313
weight: 10,
314
order: 5,
315
canToggleVisibility: true
316
});
317
318
}
319
320
return viewDescriptors;
321
}
322
323
private createSearchExtensionsViewDescriptors(): IViewDescriptor[] {
324
const viewDescriptors: IViewDescriptor[] = [];
325
326
/*
327
* View used for searching Marketplace
328
*/
329
viewDescriptors.push({
330
id: 'workbench.views.extensions.marketplace',
331
name: localize2('marketPlace', "Marketplace"),
332
ctorDescriptor: new SyncDescriptor(SearchMarketplaceExtensionsView, [{}]),
333
when: ContextKeyExpr.and(ContextKeyExpr.has('searchMarketplaceExtensions'), CONTEXT_HAS_GALLERY)
334
});
335
336
/*
337
* View used for searching all installed extensions
338
*/
339
viewDescriptors.push({
340
id: 'workbench.views.extensions.searchInstalled',
341
name: localize2('installed', "Installed"),
342
ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]),
343
when: ContextKeyExpr.or(ContextKeyExpr.has('searchInstalledExtensions'), ContextKeyExpr.has('installedExtensions')),
344
});
345
346
/*
347
* View used for searching recently updated extensions
348
*/
349
viewDescriptors.push({
350
id: 'workbench.views.extensions.searchRecentlyUpdated',
351
name: localize2('recently updated', "Recently Updated"),
352
ctorDescriptor: new SyncDescriptor(RecentlyUpdatedExtensionsView, [{}]),
353
when: ContextKeyExpr.or(SearchExtensionUpdatesContext, ContextKeyExpr.has('searchRecentlyUpdatedExtensions')),
354
order: 2,
355
});
356
357
/*
358
* View used for searching enabled extensions
359
*/
360
viewDescriptors.push({
361
id: 'workbench.views.extensions.searchEnabled',
362
name: localize2('enabled', "Enabled"),
363
ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]),
364
when: ContextKeyExpr.and(ContextKeyExpr.has('searchEnabledExtensions')),
365
});
366
367
/*
368
* View used for searching disabled extensions
369
*/
370
viewDescriptors.push({
371
id: 'workbench.views.extensions.searchDisabled',
372
name: localize2('disabled', "Disabled"),
373
ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]),
374
when: ContextKeyExpr.and(ContextKeyExpr.has('searchDisabledExtensions')),
375
});
376
377
/*
378
* View used for searching outdated extensions
379
*/
380
viewDescriptors.push({
381
id: OUTDATED_EXTENSIONS_VIEW_ID,
382
name: localize2('availableUpdates', "Available Updates"),
383
ctorDescriptor: new SyncDescriptor(OutdatedExtensionsView, [{}]),
384
when: ContextKeyExpr.or(SearchExtensionUpdatesContext, ContextKeyExpr.has('searchOutdatedExtensions')),
385
order: 1,
386
});
387
388
/*
389
* View used for searching builtin extensions
390
*/
391
viewDescriptors.push({
392
id: 'workbench.views.extensions.searchBuiltin',
393
name: localize2('builtin', "Builtin"),
394
ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]),
395
when: ContextKeyExpr.and(ContextKeyExpr.has('searchBuiltInExtensions')),
396
});
397
398
/*
399
* View used for searching workspace unsupported extensions
400
*/
401
viewDescriptors.push({
402
id: 'workbench.views.extensions.searchWorkspaceUnsupported',
403
name: localize2('workspaceUnsupported', "Workspace Unsupported"),
404
ctorDescriptor: new SyncDescriptor(ExtensionsListView, [{}]),
405
when: ContextKeyExpr.and(ContextKeyExpr.has('searchWorkspaceUnsupportedExtensions')),
406
});
407
408
return viewDescriptors;
409
}
410
411
private createRecommendedExtensionsViewDescriptors(): IViewDescriptor[] {
412
const viewDescriptors: IViewDescriptor[] = [];
413
414
viewDescriptors.push({
415
id: WORKSPACE_RECOMMENDATIONS_VIEW_ID,
416
name: localize2('workspaceRecommendedExtensions', "Workspace Recommendations"),
417
ctorDescriptor: new SyncDescriptor(WorkspaceRecommendedExtensionsView, [{}]),
418
when: ContextKeyExpr.and(ContextKeyExpr.has('recommendedExtensions'), WorkbenchStateContext.notEqualsTo('empty')),
419
order: 1
420
});
421
422
viewDescriptors.push({
423
id: 'workbench.views.extensions.otherRecommendations',
424
name: localize2('otherRecommendedExtensions', "Other Recommendations"),
425
ctorDescriptor: new SyncDescriptor(RecommendedExtensionsView, [{}]),
426
when: ContextKeyExpr.has('recommendedExtensions'),
427
order: 2
428
});
429
430
return viewDescriptors;
431
}
432
433
private createBuiltinExtensionsViewDescriptors(): IViewDescriptor[] {
434
const viewDescriptors: IViewDescriptor[] = [];
435
436
const configuredCategories = ['themes', 'programming languages'];
437
const otherCategories = EXTENSION_CATEGORIES.filter(c => !configuredCategories.includes(c.toLowerCase()));
438
otherCategories.push(NONE_CATEGORY);
439
const otherCategoriesQuery = `${otherCategories.map(c => `category:"${c}"`).join(' ')} ${configuredCategories.map(c => `category:"-${c}"`).join(' ')}`;
440
viewDescriptors.push({
441
id: 'workbench.views.extensions.builtinFeatureExtensions',
442
name: localize2('builtinFeatureExtensions', "Features"),
443
ctorDescriptor: new SyncDescriptor(StaticQueryExtensionsView, [{ query: `@builtin ${otherCategoriesQuery}` }]),
444
when: ContextKeyExpr.has('builtInExtensions'),
445
});
446
447
viewDescriptors.push({
448
id: 'workbench.views.extensions.builtinThemeExtensions',
449
name: localize2('builtInThemesExtensions', "Themes"),
450
ctorDescriptor: new SyncDescriptor(StaticQueryExtensionsView, [{ query: `@builtin category:themes` }]),
451
when: ContextKeyExpr.has('builtInExtensions'),
452
});
453
454
viewDescriptors.push({
455
id: 'workbench.views.extensions.builtinProgrammingLanguageExtensions',
456
name: localize2('builtinProgrammingLanguageExtensions', "Programming Languages"),
457
ctorDescriptor: new SyncDescriptor(StaticQueryExtensionsView, [{ query: `@builtin category:"programming languages"` }]),
458
when: ContextKeyExpr.has('builtInExtensions'),
459
});
460
461
return viewDescriptors;
462
}
463
464
private createUnsupportedWorkspaceExtensionsViewDescriptors(): IViewDescriptor[] {
465
const viewDescriptors: IViewDescriptor[] = [];
466
467
viewDescriptors.push({
468
id: 'workbench.views.extensions.untrustedUnsupportedExtensions',
469
name: localize2('untrustedUnsupportedExtensions', "Disabled in Restricted Mode"),
470
ctorDescriptor: new SyncDescriptor(UntrustedWorkspaceUnsupportedExtensionsView, [{}]),
471
when: ContextKeyExpr.and(SearchUnsupportedWorkspaceExtensionsContext),
472
});
473
474
viewDescriptors.push({
475
id: 'workbench.views.extensions.untrustedPartiallySupportedExtensions',
476
name: localize2('untrustedPartiallySupportedExtensions', "Limited in Restricted Mode"),
477
ctorDescriptor: new SyncDescriptor(UntrustedWorkspacePartiallySupportedExtensionsView, [{}]),
478
when: ContextKeyExpr.and(SearchUnsupportedWorkspaceExtensionsContext),
479
});
480
481
viewDescriptors.push({
482
id: 'workbench.views.extensions.virtualUnsupportedExtensions',
483
name: localize2('virtualUnsupportedExtensions', "Disabled in Virtual Workspaces"),
484
ctorDescriptor: new SyncDescriptor(VirtualWorkspaceUnsupportedExtensionsView, [{}]),
485
when: ContextKeyExpr.and(VirtualWorkspaceContext, SearchUnsupportedWorkspaceExtensionsContext),
486
});
487
488
viewDescriptors.push({
489
id: 'workbench.views.extensions.virtualPartiallySupportedExtensions',
490
name: localize2('virtualPartiallySupportedExtensions', "Limited in Virtual Workspaces"),
491
ctorDescriptor: new SyncDescriptor(VirtualWorkspacePartiallySupportedExtensionsView, [{}]),
492
when: ContextKeyExpr.and(VirtualWorkspaceContext, SearchUnsupportedWorkspaceExtensionsContext),
493
});
494
495
return viewDescriptors;
496
}
497
498
private createOtherLocalFilteredExtensionsViewDescriptors(): IViewDescriptor[] {
499
const viewDescriptors: IViewDescriptor[] = [];
500
501
viewDescriptors.push({
502
id: 'workbench.views.extensions.deprecatedExtensions',
503
name: localize2('deprecated', "Deprecated"),
504
ctorDescriptor: new SyncDescriptor(DeprecatedExtensionsView, [{}]),
505
when: ContextKeyExpr.and(SearchDeprecatedExtensionsContext),
506
});
507
508
return viewDescriptors;
509
}
510
511
}
512
513
export class ExtensionsViewPaneContainer extends ViewPaneContainer<IExtensionsViewletState> implements IExtensionsViewPaneContainer {
514
515
private readonly extensionsSearchValueContextKey: IContextKey<string>;
516
private readonly defaultViewsContextKey: IContextKey<boolean>;
517
private readonly sortByContextKey: IContextKey<string>;
518
private readonly searchMarketplaceExtensionsContextKey: IContextKey<boolean>;
519
private readonly searchMcpServersContextKey: IContextKey<boolean>;
520
private readonly searchHasTextContextKey: IContextKey<boolean>;
521
private readonly sortByUpdateDateContextKey: IContextKey<boolean>;
522
private readonly installedExtensionsContextKey: IContextKey<boolean>;
523
private readonly searchInstalledExtensionsContextKey: IContextKey<boolean>;
524
private readonly searchRecentlyUpdatedExtensionsContextKey: IContextKey<boolean>;
525
private readonly searchExtensionUpdatesContextKey: IContextKey<boolean>;
526
private readonly searchOutdatedExtensionsContextKey: IContextKey<boolean>;
527
private readonly searchEnabledExtensionsContextKey: IContextKey<boolean>;
528
private readonly searchDisabledExtensionsContextKey: IContextKey<boolean>;
529
private readonly hasInstalledExtensionsContextKey: IContextKey<boolean>;
530
private readonly builtInExtensionsContextKey: IContextKey<boolean>;
531
private readonly searchBuiltInExtensionsContextKey: IContextKey<boolean>;
532
private readonly searchWorkspaceUnsupportedExtensionsContextKey: IContextKey<boolean>;
533
private readonly searchDeprecatedExtensionsContextKey: IContextKey<boolean>;
534
private readonly recommendedExtensionsContextKey: IContextKey<boolean>;
535
536
private searchDelayer: Delayer<void>;
537
private root: HTMLElement | undefined;
538
private header: HTMLElement | undefined;
539
private searchBox: SuggestEnabledInput | undefined;
540
private notificationContainer: HTMLElement | undefined;
541
private readonly searchViewletState: IExtensionsViewletState;
542
private extensionGalleryManifest: IExtensionGalleryManifest | null = null;
543
544
constructor(
545
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
546
@ITelemetryService telemetryService: ITelemetryService,
547
@IProgressService private readonly progressService: IProgressService,
548
@IInstantiationService instantiationService: IInstantiationService,
549
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
550
@IExtensionGalleryManifestService extensionGalleryManifestService: IExtensionGalleryManifestService,
551
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
552
@IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService,
553
@INotificationService private readonly notificationService: INotificationService,
554
@IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService,
555
@IThemeService themeService: IThemeService,
556
@IConfigurationService configurationService: IConfigurationService,
557
@IStorageService storageService: IStorageService,
558
@IWorkspaceContextService contextService: IWorkspaceContextService,
559
@IContextKeyService private readonly contextKeyService: IContextKeyService,
560
@IContextMenuService contextMenuService: IContextMenuService,
561
@IExtensionService extensionService: IExtensionService,
562
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
563
@IPreferencesService private readonly preferencesService: IPreferencesService,
564
@ICommandService private readonly commandService: ICommandService,
565
@ILogService logService: ILogService,
566
@IHoverService private readonly hoverService: IHoverService,
567
) {
568
super(VIEWLET_ID, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService, logService);
569
570
this.searchDelayer = new Delayer(500);
571
this.extensionsSearchValueContextKey = ExtensionsSearchValueContext.bindTo(contextKeyService);
572
this.defaultViewsContextKey = DefaultViewsContext.bindTo(contextKeyService);
573
this.sortByContextKey = ExtensionsSortByContext.bindTo(contextKeyService);
574
this.searchMarketplaceExtensionsContextKey = SearchMarketplaceExtensionsContext.bindTo(contextKeyService);
575
this.searchMcpServersContextKey = SearchMcpServersContext.bindTo(contextKeyService);
576
this.searchHasTextContextKey = SearchHasTextContext.bindTo(contextKeyService);
577
this.sortByUpdateDateContextKey = SortByUpdateDateContext.bindTo(contextKeyService);
578
this.installedExtensionsContextKey = InstalledExtensionsContext.bindTo(contextKeyService);
579
this.searchInstalledExtensionsContextKey = SearchInstalledExtensionsContext.bindTo(contextKeyService);
580
this.searchRecentlyUpdatedExtensionsContextKey = SearchRecentlyUpdatedExtensionsContext.bindTo(contextKeyService);
581
this.searchExtensionUpdatesContextKey = SearchExtensionUpdatesContext.bindTo(contextKeyService);
582
this.searchWorkspaceUnsupportedExtensionsContextKey = SearchUnsupportedWorkspaceExtensionsContext.bindTo(contextKeyService);
583
this.searchDeprecatedExtensionsContextKey = SearchDeprecatedExtensionsContext.bindTo(contextKeyService);
584
this.searchOutdatedExtensionsContextKey = SearchOutdatedExtensionsContext.bindTo(contextKeyService);
585
this.searchEnabledExtensionsContextKey = SearchEnabledExtensionsContext.bindTo(contextKeyService);
586
this.searchDisabledExtensionsContextKey = SearchDisabledExtensionsContext.bindTo(contextKeyService);
587
this.hasInstalledExtensionsContextKey = HasInstalledExtensionsContext.bindTo(contextKeyService);
588
this.builtInExtensionsContextKey = BuiltInExtensionsContext.bindTo(contextKeyService);
589
this.searchBuiltInExtensionsContextKey = SearchBuiltInExtensionsContext.bindTo(contextKeyService);
590
this.recommendedExtensionsContextKey = RecommendedExtensionsContext.bindTo(contextKeyService);
591
this._register(this.paneCompositeService.onDidPaneCompositeOpen(e => { if (e.viewContainerLocation === ViewContainerLocation.Sidebar) { this.onViewletOpen(e.composite); } }, this));
592
this._register(extensionsWorkbenchService.onReset(() => this.refresh()));
593
this.searchViewletState = this.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE);
594
595
extensionGalleryManifestService.getExtensionGalleryManifest()
596
.then(galleryManifest => {
597
this.extensionGalleryManifest = galleryManifest;
598
this._register(extensionGalleryManifestService.onDidChangeExtensionGalleryManifest(galleryManifest => {
599
this.extensionGalleryManifest = galleryManifest;
600
this.refresh();
601
}));
602
});
603
}
604
605
get searchValue(): string | undefined {
606
return this.searchBox?.getValue();
607
}
608
609
override create(parent: HTMLElement): void {
610
parent.classList.add('extensions-viewlet');
611
this.root = parent;
612
613
const overlay = append(this.root, $('.overlay'));
614
const overlayBackgroundColor = this.getColor(SIDE_BAR_DRAG_AND_DROP_BACKGROUND) ?? '';
615
overlay.style.backgroundColor = overlayBackgroundColor;
616
hide(overlay);
617
618
this.header = append(this.root, $('.header'));
619
const placeholder = localize('searchExtensions', "Search Extensions in Marketplace");
620
621
const searchValue = this.searchViewletState['query.value'] ? this.searchViewletState['query.value'] : '';
622
623
const searchContainer = append(this.header, $('.extensions-search-container'));
624
625
this.searchBox = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${VIEWLET_ID}.searchbox`, searchContainer, {
626
triggerCharacters: ['@'],
627
sortKey: (item: string) => {
628
if (item.indexOf(':') === -1) { return 'a'; }
629
else if (/ext:/.test(item) || /id:/.test(item) || /tag:/.test(item)) { return 'b'; }
630
else if (/sort:/.test(item)) { return 'c'; }
631
else { return 'd'; }
632
},
633
provideResults: (query: string) => Query.suggestions(query, this.extensionGalleryManifest)
634
}, placeholder, 'extensions:searchinput', { placeholderText: placeholder, value: searchValue }));
635
636
this.notificationContainer = append(this.header, $('.notification-container.hidden', { 'tabindex': '0' }));
637
this.renderNotificaiton();
638
this._register(this.extensionsWorkbenchService.onDidChangeExtensionsNotification(() => this.renderNotificaiton()));
639
640
this.updateInstalledExtensionsContexts();
641
if (this.searchBox.getValue()) {
642
this.triggerSearch();
643
}
644
645
this._register(this.searchBox.onInputDidChange(() => {
646
this.sortByContextKey.set(Query.parse(this.searchBox?.getValue() ?? '').sortBy);
647
this.triggerSearch();
648
}, this));
649
650
this._register(this.searchBox.onShouldFocusResults(() => this.focusListView(), this));
651
652
const controlElement = append(searchContainer, $('.extensions-search-actions-container'));
653
this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, controlElement, extensionsSearchActionsMenu, {
654
toolbarOptions: {
655
primaryGroup: () => true,
656
},
657
actionViewItemProvider: (action, options) => createActionViewItem(this.instantiationService, action, options)
658
}));
659
660
// Register DragAndDrop support
661
this._register(new DragAndDropObserver(this.root, {
662
onDragEnter: (e: DragEvent) => {
663
if (this.isSupportedDragElement(e)) {
664
show(overlay);
665
}
666
},
667
onDragLeave: (e: DragEvent) => {
668
if (this.isSupportedDragElement(e)) {
669
hide(overlay);
670
}
671
},
672
onDragOver: (e: DragEvent) => {
673
if (this.isSupportedDragElement(e)) {
674
e.dataTransfer!.dropEffect = 'copy';
675
}
676
},
677
onDrop: async (e: DragEvent) => {
678
if (this.isSupportedDragElement(e)) {
679
hide(overlay);
680
681
const vsixs = coalesce((await this.instantiationService.invokeFunction(accessor => extractEditorsAndFilesDropData(accessor, e)))
682
.map(editor => editor.resource && extname(editor.resource) === '.vsix' ? editor.resource : undefined));
683
684
if (vsixs.length > 0) {
685
try {
686
// Attempt to install the extension(s)
687
await this.commandService.executeCommand(INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, vsixs);
688
}
689
catch (err) {
690
this.notificationService.error(err);
691
}
692
}
693
}
694
}
695
}));
696
697
super.create(append(this.root, $('.extensions')));
698
699
const focusTracker = this._register(trackFocus(this.root));
700
const isSearchBoxFocused = () => this.searchBox?.inputWidget.hasWidgetFocus();
701
this._register(registerNavigableContainer({
702
name: 'extensionsView',
703
focusNotifiers: [focusTracker],
704
focusNextWidget: () => {
705
if (isSearchBoxFocused()) {
706
this.focusListView();
707
}
708
},
709
focusPreviousWidget: () => {
710
if (!isSearchBoxFocused()) {
711
this.searchBox?.focus();
712
}
713
}
714
}));
715
}
716
717
override focus(): void {
718
super.focus();
719
this.searchBox?.focus();
720
}
721
722
private _dimension: Dimension | undefined;
723
override layout(dimension: Dimension): void {
724
this._dimension = dimension;
725
if (this.root) {
726
this.root.classList.toggle('narrow', dimension.width <= 250);
727
this.root.classList.toggle('mini', dimension.width <= 200);
728
}
729
this.searchBox?.layout(new Dimension(dimension.width - 34 - /*padding*/8 - (24 * 2), 20));
730
const searchBoxHeight = 20 + 21 /*margin*/;
731
const headerHeight = this.header && !!this.notificationContainer?.childNodes.length ? this.notificationContainer.clientHeight + searchBoxHeight + 10 /*margin*/ : searchBoxHeight;
732
this.header!.style.height = `${headerHeight}px`;
733
super.layout(new Dimension(dimension.width, dimension.height - headerHeight));
734
}
735
736
override getOptimalWidth(): number {
737
return 400;
738
}
739
740
search(value: string): void {
741
if (this.searchBox && this.searchBox.getValue() !== value) {
742
this.searchBox.setValue(value);
743
}
744
}
745
746
async refresh(): Promise<void> {
747
await this.updateInstalledExtensionsContexts();
748
this.doSearch(true);
749
if (this.configurationService.getValue(AutoCheckUpdatesConfigurationKey)) {
750
this.extensionsWorkbenchService.checkForUpdates();
751
}
752
}
753
754
private readonly notificationDisposables = this._register(new MutableDisposable<DisposableStore>());
755
private renderNotificaiton(): void {
756
if (!this.notificationContainer) {
757
return;
758
}
759
760
clearNode(this.notificationContainer);
761
this.notificationDisposables.value = new DisposableStore();
762
const status = this.extensionsWorkbenchService.getExtensionsNotification();
763
const query = status?.extensions.map(extension => `@id:${extension.identifier.id}`).join(' ');
764
if (status && (query === this.searchBox?.getValue() || !this.searchMarketplaceExtensionsContextKey.get())) {
765
this.notificationContainer.setAttribute('aria-label', status.message);
766
this.notificationContainer.classList.remove('hidden');
767
const messageContainer = append(this.notificationContainer, $('.message-container'));
768
append(messageContainer, $('span')).className = SeverityIcon.className(status.severity);
769
append(messageContainer, $('span.message', undefined, status.message));
770
const showAction = append(messageContainer,
771
$('span.message-text-action', {
772
'tabindex': '0',
773
'role': 'button',
774
'aria-label': `${status.message}. ${localize('click show', "Click to Show")}`
775
}, localize('show', "Show")));
776
this.notificationDisposables.value.add(addDisposableListener(showAction, EventType.CLICK, () => this.search(query ?? '')));
777
this.notificationDisposables.value.add(addDisposableListener(showAction, EventType.KEY_DOWN, (e: KeyboardEvent) => {
778
const standardKeyboardEvent = new StandardKeyboardEvent(e);
779
if (standardKeyboardEvent.keyCode === KeyCode.Enter || standardKeyboardEvent.keyCode === KeyCode.Space) {
780
this.search(query ?? '');
781
}
782
standardKeyboardEvent.stopPropagation();
783
}));
784
const dismissAction = append(this.notificationContainer,
785
$(`span.message-action${ThemeIcon.asCSSSelector(Codicon.close)}`, {
786
'tabindex': '0',
787
'role': 'button',
788
'aria-label': localize('dismiss', "Dismiss"),
789
}));
790
this.notificationDisposables.value.add(this.hoverService.setupDelayedHover(dismissAction, { content: localize('dismiss hover', "Dismiss") }));
791
this.notificationDisposables.value.add(addDisposableListener(dismissAction, EventType.CLICK, () => status.dismiss()));
792
this.notificationDisposables.value.add(addDisposableListener(dismissAction, EventType.KEY_DOWN, (e: KeyboardEvent) => {
793
const standardKeyboardEvent = new StandardKeyboardEvent(e);
794
if (standardKeyboardEvent.keyCode === KeyCode.Enter || standardKeyboardEvent.keyCode === KeyCode.Space) {
795
status.dismiss();
796
}
797
standardKeyboardEvent.stopPropagation();
798
}));
799
} else {
800
this.notificationContainer.removeAttribute('aria-label');
801
this.notificationContainer.classList.add('hidden');
802
}
803
804
if (this._dimension) {
805
this.layout(this._dimension);
806
}
807
}
808
809
private async updateInstalledExtensionsContexts(): Promise<void> {
810
const result = await this.extensionsWorkbenchService.queryLocal();
811
this.hasInstalledExtensionsContextKey.set(result.some(r => !r.isBuiltin));
812
}
813
814
private triggerSearch(): void {
815
this.searchDelayer.trigger(() => this.doSearch(), this.searchBox && this.searchBox.getValue() ? 500 : 0).then(undefined, err => this.onError(err));
816
}
817
818
private normalizedQuery(): string {
819
return this.searchBox
820
? this.searchBox.getValue()
821
.trim()
822
.replace(/@category/g, 'category')
823
.replace(/@tag:/g, 'tag:')
824
.replace(/@ext:/g, 'ext:')
825
.replace(/@featured/g, 'featured')
826
.replace(/@popular/g, this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer && !this.extensionManagementServerService.remoteExtensionManagementServer ? '@web' : '@popular')
827
: '';
828
}
829
830
protected override saveState(): void {
831
const value = this.searchBox ? this.searchBox.getValue() : '';
832
if (ExtensionsListView.isLocalExtensionsQuery(value)) {
833
this.searchViewletState['query.value'] = value;
834
} else {
835
this.searchViewletState['query.value'] = '';
836
}
837
super.saveState();
838
}
839
840
private doSearch(refresh?: boolean): Promise<void> {
841
const value = this.normalizedQuery();
842
this.contextKeyService.bufferChangeEvents(() => {
843
const isRecommendedExtensionsQuery = ExtensionsListView.isRecommendedExtensionsQuery(value);
844
this.searchHasTextContextKey.set(value.trim() !== '');
845
this.extensionsSearchValueContextKey.set(value);
846
this.installedExtensionsContextKey.set(ExtensionsListView.isInstalledExtensionsQuery(value));
847
this.searchInstalledExtensionsContextKey.set(ExtensionsListView.isSearchInstalledExtensionsQuery(value));
848
this.searchRecentlyUpdatedExtensionsContextKey.set(ExtensionsListView.isSearchRecentlyUpdatedQuery(value) && !ExtensionsListView.isSearchExtensionUpdatesQuery(value));
849
this.searchOutdatedExtensionsContextKey.set(ExtensionsListView.isOutdatedExtensionsQuery(value) && !ExtensionsListView.isSearchExtensionUpdatesQuery(value));
850
this.searchExtensionUpdatesContextKey.set(ExtensionsListView.isSearchExtensionUpdatesQuery(value));
851
this.searchEnabledExtensionsContextKey.set(ExtensionsListView.isEnabledExtensionsQuery(value));
852
this.searchDisabledExtensionsContextKey.set(ExtensionsListView.isDisabledExtensionsQuery(value));
853
this.searchBuiltInExtensionsContextKey.set(ExtensionsListView.isSearchBuiltInExtensionsQuery(value));
854
this.searchWorkspaceUnsupportedExtensionsContextKey.set(ExtensionsListView.isSearchWorkspaceUnsupportedExtensionsQuery(value));
855
this.searchDeprecatedExtensionsContextKey.set(ExtensionsListView.isSearchDeprecatedExtensionsQuery(value));
856
this.builtInExtensionsContextKey.set(ExtensionsListView.isBuiltInExtensionsQuery(value));
857
this.recommendedExtensionsContextKey.set(isRecommendedExtensionsQuery);
858
this.searchMcpServersContextKey.set(!!value && /@mcp\s?.*/i.test(value));
859
this.searchMarketplaceExtensionsContextKey.set(!!value && !ExtensionsListView.isLocalExtensionsQuery(value) && !isRecommendedExtensionsQuery && !this.searchMcpServersContextKey.get());
860
this.sortByUpdateDateContextKey.set(ExtensionsListView.isSortUpdateDateQuery(value));
861
this.defaultViewsContextKey.set(!value || ExtensionsListView.isSortInstalledExtensionsQuery(value));
862
});
863
864
this.renderNotificaiton();
865
866
return this.showExtensionsViews(this.panes);
867
}
868
869
protected override onDidAddViewDescriptors(added: IAddedViewDescriptorRef[]): ViewPane[] {
870
const addedViews = super.onDidAddViewDescriptors(added);
871
this.showExtensionsViews(addedViews);
872
return addedViews;
873
}
874
875
private async showExtensionsViews(views: ViewPane[]): Promise<void> {
876
await this.progress(Promise.all(views.map(async view => {
877
if (view instanceof AbstractExtensionsListView) {
878
const model = await view.show(this.normalizedQuery());
879
this.alertSearchResult(model.length, view.id);
880
}
881
})));
882
}
883
884
private alertSearchResult(count: number, viewId: string): void {
885
const view = this.viewContainerModel.visibleViewDescriptors.find(view => view.id === viewId);
886
switch (count) {
887
case 0:
888
break;
889
case 1:
890
if (view) {
891
alert(localize('extensionFoundInSection', "1 extension found in the {0} section.", view.name.value));
892
} else {
893
alert(localize('extensionFound', "1 extension found."));
894
}
895
break;
896
default:
897
if (view) {
898
alert(localize('extensionsFoundInSection', "{0} extensions found in the {1} section.", count, view.name.value));
899
} else {
900
alert(localize('extensionsFound', "{0} extensions found.", count));
901
}
902
break;
903
}
904
}
905
906
private getFirstExpandedPane(): ExtensionsListView | undefined {
907
for (const pane of this.panes) {
908
if (pane.isExpanded() && pane instanceof ExtensionsListView) {
909
return pane;
910
}
911
}
912
return undefined;
913
}
914
915
private focusListView(): void {
916
const pane = this.getFirstExpandedPane();
917
if (pane && pane.count() > 0) {
918
pane.focus();
919
}
920
}
921
922
private onViewletOpen(viewlet: IPaneComposite): void {
923
if (!viewlet || viewlet.getId() === VIEWLET_ID) {
924
return;
925
}
926
927
if (this.configurationService.getValue<boolean>(CloseExtensionDetailsOnViewChangeKey)) {
928
const promises = this.editorGroupService.groups.map(group => {
929
const editors = group.editors.filter(input => input instanceof ExtensionsInput);
930
931
return group.closeEditors(editors);
932
});
933
934
Promise.all(promises);
935
}
936
}
937
938
private progress<T>(promise: Promise<T>): Promise<T> {
939
return this.progressService.withProgress({ location: ProgressLocation.Extensions }, () => promise);
940
}
941
942
private onError(err: Error): void {
943
if (isCancellationError(err)) {
944
return;
945
}
946
947
const message = err && err.message || '';
948
949
if (/ECONNREFUSED/.test(message)) {
950
const error = createErrorWithActions(localize('suggestProxyError', "Marketplace returned 'ECONNREFUSED'. Please check the 'http.proxy' setting."), [
951
new Action('open user settings', localize('open user settings', "Open User Settings"), undefined, true, () => this.preferencesService.openUserSettings())
952
]);
953
954
this.notificationService.error(error);
955
return;
956
}
957
958
this.notificationService.error(err);
959
}
960
961
private isSupportedDragElement(e: DragEvent): boolean {
962
if (e.dataTransfer) {
963
const typesLowerCase = e.dataTransfer.types.map(t => t.toLocaleLowerCase());
964
return typesLowerCase.indexOf('files') !== -1;
965
}
966
967
return false;
968
}
969
}
970
971
export class StatusUpdater extends Disposable implements IWorkbenchContribution {
972
973
private readonly badgeHandle = this._register(new MutableDisposable());
974
975
constructor(
976
@IActivityService private readonly activityService: IActivityService,
977
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
978
@IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService,
979
@IConfigurationService private readonly configurationService: IConfigurationService
980
) {
981
super();
982
this.onServiceChange();
983
this._register(Event.any(Event.debounce(extensionsWorkbenchService.onChange, () => undefined, 100, undefined, undefined, undefined, this._store), extensionsWorkbenchService.onDidChangeExtensionsNotification)(this.onServiceChange, this));
984
}
985
986
private onServiceChange(): void {
987
this.badgeHandle.clear();
988
let badge: IBadge | undefined;
989
990
const extensionsNotification = this.extensionsWorkbenchService.getExtensionsNotification();
991
if (extensionsNotification) {
992
if (extensionsNotification.severity === Severity.Warning) {
993
badge = new WarningBadge(() => extensionsNotification.message);
994
}
995
}
996
997
else {
998
const actionRequired = this.configurationService.getValue(AutoRestartConfigurationKey) === true ? [] : this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined);
999
const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0);
1000
const newBadgeNumber = outdated + actionRequired.length;
1001
if (newBadgeNumber > 0) {
1002
let msg = '';
1003
if (outdated) {
1004
msg += outdated === 1 ? localize('extensionToUpdate', '{0} requires update', outdated) : localize('extensionsToUpdate', '{0} require update', outdated);
1005
}
1006
if (outdated > 0 && actionRequired.length > 0) {
1007
msg += ', ';
1008
}
1009
if (actionRequired.length) {
1010
msg += actionRequired.length === 1 ? localize('extensionToReload', '{0} requires restart', actionRequired.length) : localize('extensionsToReload', '{0} require restart', actionRequired.length);
1011
}
1012
badge = new NumberBadge(newBadgeNumber, () => msg);
1013
}
1014
}
1015
1016
if (badge) {
1017
this.badgeHandle.value = this.activityService.showViewContainerActivity(VIEWLET_ID, { badge });
1018
}
1019
}
1020
}
1021
1022
export class MaliciousExtensionChecker implements IWorkbenchContribution {
1023
1024
constructor(
1025
@IExtensionManagementService private readonly extensionsManagementService: IExtensionManagementService,
1026
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
1027
@IHostService private readonly hostService: IHostService,
1028
@ILogService private readonly logService: ILogService,
1029
@INotificationService private readonly notificationService: INotificationService,
1030
@ICommandService private readonly commandService: ICommandService,
1031
) {
1032
this.loopCheckForMaliciousExtensions();
1033
}
1034
1035
private loopCheckForMaliciousExtensions(): void {
1036
this.checkForMaliciousExtensions()
1037
.then(() => timeout(1000 * 60 * 5)) // every five minutes
1038
.then(() => this.loopCheckForMaliciousExtensions());
1039
}
1040
1041
private async checkForMaliciousExtensions(): Promise<void> {
1042
try {
1043
const maliciousExtensions: [ILocalExtension, string | undefined][] = [];
1044
let shouldRestartExtensions = false;
1045
let shouldReloadWindow = false;
1046
for (const extension of this.extensionsWorkbenchService.installed) {
1047
if (extension.isMalicious && extension.local) {
1048
maliciousExtensions.push([extension.local, extension.maliciousInfoLink]);
1049
shouldRestartExtensions = shouldRestartExtensions || extension.runtimeState?.action === ExtensionRuntimeActionType.RestartExtensions;
1050
shouldReloadWindow = shouldReloadWindow || extension.runtimeState?.action === ExtensionRuntimeActionType.ReloadWindow;
1051
}
1052
}
1053
if (maliciousExtensions.length) {
1054
await this.extensionsManagementService.uninstallExtensions(maliciousExtensions.map(e => ({ extension: e[0], options: { remove: true } })));
1055
for (const [extension, link] of maliciousExtensions) {
1056
const buttons: IPromptChoice[] = [];
1057
if (shouldRestartExtensions || shouldReloadWindow) {
1058
buttons.push({
1059
label: shouldRestartExtensions ? localize('restartNow', "Restart Extensions") : localize('reloadNow', "Reload Now"),
1060
run: () => shouldRestartExtensions ? this.extensionsWorkbenchService.updateRunningExtensions() : this.hostService.reload()
1061
});
1062
}
1063
if (link) {
1064
buttons.push({
1065
label: localize('learnMore', "Learn More"),
1066
run: () => this.commandService.executeCommand('vscode.open', URI.parse(link))
1067
});
1068
}
1069
this.notificationService.prompt(
1070
Severity.Warning,
1071
localize('malicious warning', "The extension '{0}' was found to be problematic and has been uninstalled", extension.manifest.displayName || extension.identifier.id),
1072
buttons,
1073
{
1074
sticky: true,
1075
priority: NotificationPriority.URGENT
1076
}
1077
);
1078
}
1079
}
1080
1081
} catch (err) {
1082
this.logService.error(err);
1083
}
1084
}
1085
}
1086
1087
export class ExtensionMarketplaceStatusUpdater extends Disposable implements IWorkbenchContribution {
1088
1089
private readonly badgeHandle = this._register(new MutableDisposable());
1090
private readonly accountBadgeDisposable = this._register(new MutableDisposable());
1091
1092
constructor(
1093
@IActivityService private readonly activityService: IActivityService,
1094
@IExtensionGalleryManifestService private readonly extensionGalleryManifestService: IExtensionGalleryManifestService
1095
) {
1096
super();
1097
this.updateBadge();
1098
this._register(this.extensionGalleryManifestService.onDidChangeExtensionGalleryManifestStatus(() => this.updateBadge()));
1099
}
1100
1101
private async updateBadge(): Promise<void> {
1102
this.badgeHandle.clear();
1103
1104
const status = this.extensionGalleryManifestService.extensionGalleryManifestStatus;
1105
let badge: IBadge | undefined;
1106
1107
switch (status) {
1108
case ExtensionGalleryManifestStatus.RequiresSignIn:
1109
badge = new NumberBadge(1, () => localize('signInRequired', "Sign in required to access marketplace"));
1110
break;
1111
case ExtensionGalleryManifestStatus.AccessDenied:
1112
badge = new WarningBadge(() => localize('accessDenied', "Access denied to marketplace"));
1113
break;
1114
}
1115
1116
if (badge) {
1117
this.badgeHandle.value = this.activityService.showViewContainerActivity(VIEWLET_ID, { badge });
1118
}
1119
1120
this.accountBadgeDisposable.clear();
1121
if (status === ExtensionGalleryManifestStatus.RequiresSignIn) {
1122
const badge = new NumberBadge(1, () => localize('sign in enterprise marketplace', "Sign in to access Marketplace"));
1123
this.accountBadgeDisposable.value = this.activityService.showAccountsActivity({ badge });
1124
}
1125
}
1126
}
1127
1128