This commit is contained in:
Sandeep Somavarapu 2020-11-09 15:38:35 +01:00
parent 23579d815f
commit 294406d7a1
9 changed files with 487 additions and 447 deletions

View file

@ -365,6 +365,10 @@
{
"name": "vs/workbench/services/authentication",
"project": "vscode-workbench"
},
{
"name": "vs/workbench/services/extensionRecommendations",
"project": "vscode-workbench"
}
]
}

View file

@ -100,7 +100,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
})
]);
this._register(this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations(() => this._onDidChangeRecommendations.fire()));
this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations, this.extensionRecommendationsManagementService.onDidChangeIgnoredRecommendations)(() => this._onDidChangeRecommendations.fire()));
this._register(this.extensionRecommendationsManagementService.onDidChangeGlobalIgnoredRecommendation(({ extensionId, isRecommended }) => {
if (!isRecommended) {
const reason = this.getAllRecommendationsWithReason()[extensionId];
@ -111,7 +111,6 @@ export class ExtensionRecommendationsService extends Disposable implements IExte
}));
await this.promptWorkspaceRecommendations();
this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations)(() => this.promptWorkspaceRecommendations()));
}
private isEnabled(): boolean {

View file

@ -10,7 +10,7 @@ import { MenuRegistry, MenuId, registerAction2, Action2 } from 'vs/platform/acti
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ExtensionsLabel, ExtensionsLocalizedLabel, ExtensionsChannelId, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
@ -18,7 +18,7 @@ import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer,
import {
OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction,
ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction,
EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction
EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction
} from 'vs/workbench/contrib/extensions/browser/extensionsActions';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor';
@ -26,7 +26,7 @@ import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContrib
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
import * as jsonContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { ExtensionsConfigurationSchema, ExtensionsConfigurationSchemaId } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeymapExtensions } from 'vs/workbench/contrib/extensions/common/extensionsUtils';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
@ -62,6 +62,8 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { ResourceContextKey } from 'vs/workbench/common/resources';
import { IAction } from 'vs/base/common/actions';
import { IWorkpsaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig';
import { Schemas } from 'vs/base/common/network';
// Singletons
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService);
@ -929,6 +931,220 @@ class ExtensionsContributions implements IWorkbenchContribution {
}
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.extensions.action.ignoreRecommendation',
title: { value: localize('workbench.extensions.action.ignoreRecommendation', "Ignore Recommendation"), original: `Ignore Recommendation` },
menu: {
id: MenuId.ExtensionContext,
group: '3_recommendations',
when: ContextKeyExpr.has('isExtensionRecommended'),
order: 1
},
});
}
async run(accessor: ServicesAccessor, id: string): Promise<any> {
accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, true);
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.extensions.action.undoIgnoredRecommendation',
title: { value: localize('workbench.extensions.action.undoIgnoredRecommendation', "Undo Ignored Recommendation"), original: `Undo Ignored Recommendation` },
menu: {
id: MenuId.ExtensionContext,
group: '3_recommendations',
when: ContextKeyExpr.has('isUserIgnoredRecommendation'),
order: 1
},
});
}
async run(accessor: ServicesAccessor, id: string): Promise<any> {
accessor.get(IExtensionIgnoredRecommendationsService).toggleGlobalIgnoredRecommendation(id, false);
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.extensions.action.addExtensionToWorkspaceRecommendations',
title: { value: localize('workbench.extensions.action.addExtensionToWorkspaceRecommendations', "Add to Workspace Recommendations"), original: `Add to Workspace Recommendations` },
menu: {
id: MenuId.ExtensionContext,
group: '3_recommendations',
when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended').negate(), ContextKeyExpr.has('isUserIgnoredRecommendation').negate()),
order: 2
},
});
}
run(accessor: ServicesAccessor, id: string): Promise<any> {
return accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id);
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.extensions.action.removeExtensionFromWorkspaceRecommendations',
title: { value: localize('workbench.extensions.action.removeExtensionFromWorkspaceRecommendations', "Remove from Workspace Recommendations"), original: `Remove from Workspace Recommendations` },
menu: {
id: MenuId.ExtensionContext,
group: '3_recommendations',
when: ContextKeyExpr.and(WorkbenchStateContext.notEqualsTo('empty'), ContextKeyExpr.has('isBuiltinExtension').negate(), ContextKeyExpr.has('isExtensionWorkspaceRecommended')),
order: 2
},
});
}
run(accessor: ServicesAccessor, id: string): Promise<any> {
return accessor.get(IWorkpsaceExtensionsConfigService).toggleRecommendation(id);
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.extensions.action.addToWorkspaceRecommendations',
title: { value: localize('workbench.extensions.action.addToWorkspaceRecommendations', "Add Extension to Workspace Recommendations"), original: `Add Extension to Workspace Recommendations` },
category: localize('extensions', "Extensions"),
menu: {
id: MenuId.CommandPalette,
when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)),
},
});
}
async run(accessor: ServicesAccessor): Promise<any> {
const editorService = accessor.get(IEditorService);
const workpsaceExtensionsConfigService = accessor.get(IWorkpsaceExtensionsConfigService);
if (!(editorService.activeEditor instanceof ExtensionsInput)) {
return;
}
const extensionId = editorService.activeEditor.extension.identifier.id.toLowerCase();
const recommendations = await workpsaceExtensionsConfigService.getRecommendations();
if (recommendations.includes(extensionId)) {
return;
}
await workpsaceExtensionsConfigService.toggleRecommendation(extensionId);
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.extensions.action.addToWorkspaceFolderRecommendations',
title: { value: localize('workbench.extensions.action.addToWorkspaceFolderRecommendations', "Add Extension to Workspace Folder Recommendations"), original: `Add Extension to Workspace Folder Recommendations` },
category: localize('extensions', "Extensions"),
menu: {
id: MenuId.CommandPalette,
when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)),
},
});
}
async run(accessor: ServicesAccessor): Promise<any> {
return accessor.get(ICommandService).executeCommand('workbench.extensions.action.addToWorkspaceRecommendations');
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.extensions.action.addToWorkspaceIgnoredRecommendations',
title: { value: localize('workbench.extensions.action.addToWorkspaceIgnoredRecommendations', "Add Extension to Workspace Ignored Recommendations"), original: `Add Extension to Workspace Ignored Recommendations` },
category: localize('extensions', "Extensions"),
menu: {
id: MenuId.CommandPalette,
when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)),
},
});
}
async run(accessor: ServicesAccessor): Promise<any> {
const editorService = accessor.get(IEditorService);
const workpsaceExtensionsConfigService = accessor.get(IWorkpsaceExtensionsConfigService);
if (!(editorService.activeEditor instanceof ExtensionsInput)) {
return;
}
const extensionId = editorService.activeEditor.extension.identifier.id.toLowerCase();
const unwatedRecommendations = await workpsaceExtensionsConfigService.getUnwantedRecommendations();
if (unwatedRecommendations.includes(extensionId)) {
return;
}
await workpsaceExtensionsConfigService.toggleUnwantedRecommendation(extensionId);
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations',
title: { value: localize('workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations', "Add Extension to Workspace Folder Ignored Recommendations"), original: `Add Extension to Workspace Folder Ignored Recommendations` },
category: localize('extensions', "Extensions"),
menu: {
id: MenuId.CommandPalette,
when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('folder'), ContextKeyExpr.equals('resourceScheme', Schemas.extension)),
},
});
}
run(accessor: ServicesAccessor): Promise<any> {
return accessor.get(ICommandService).executeCommand('workbench.extensions.action.addToWorkspaceIgnoredRecommendations');
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: ConfigureWorkspaceRecommendedExtensionsAction.ID,
title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' },
category: localize('extensions', "Extensions"),
menu: {
id: MenuId.CommandPalette,
when: WorkbenchStateContext.isEqualTo('workspace'),
},
});
}
run(accessor: ServicesAccessor): Promise<any> {
return accessor.get(IInstantiationService).createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL).run();
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID,
title: { value: ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace Folder)' },
category: localize('extensions', "Extensions"),
menu: {
id: MenuId.CommandPalette,
when: WorkbenchStateContext.notEqualsTo('empty'),
},
});
}
run(accessor: ServicesAccessor): Promise<any> {
return accessor.get(IInstantiationService).createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL).run();
}
});
}
}
@ -936,7 +1152,6 @@ const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(Workbench
workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Starting);
workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Restored);
workbenchRegistry.registerWorkbenchContribution(MaliciousExtensionChecker, LifecyclePhase.Eventually);
workbenchRegistry.registerWorkbenchContribution(ConfigureRecommendedExtensionsCommandsContributor, LifecyclePhase.Eventually);
workbenchRegistry.registerWorkbenchContribution(KeymapExtensions, LifecyclePhase.Restored);
workbenchRegistry.registerWorkbenchContribution(ExtensionsViewletViewsContribution, LifecyclePhase.Starting);
workbenchRegistry.registerWorkbenchContribution(ExtensionActivationProgress, LifecyclePhase.Eventually);

View file

@ -11,12 +11,12 @@ import * as DOM from 'vs/base/browser/dom';
import { Event } from 'vs/base/common/event';
import * as json from 'vs/base/common/json';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { dispose, Disposable } from 'vs/base/common/lifecycle';
import { dispose } from 'vs/base/common/lifecycle';
import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, AutoUpdateConfigurationKey, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID } from 'vs/workbench/contrib/extensions/common/extensions';
import { ExtensionsConfigurationInitialContent } from 'vs/workbench/contrib/extensions/common/extensionsFileTemplate';
import { IGalleryExtension, IExtensionGalleryService, INSTALL_ERROR_MALICIOUS, INSTALL_ERROR_INCOMPATIBLE, IGalleryExtensionVersion, ILocalExtension, INSTALL_ERROR_NOT_SUPPORTED, InstallOptions, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionIgnoredRecommendationsService, IExtensionsConfigContent } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { ExtensionRecommendationReason, IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ExtensionType, ExtensionIdentifier, IExtensionDescription, IExtensionManifest, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
@ -36,16 +36,14 @@ import { Color } from 'vs/base/common/color';
import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing';
import { ITextEditorSelection } from 'vs/platform/editor/common/editor';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { MenuRegistry, MenuId, IMenuService } from 'vs/platform/actions/common/actions';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { MenuId, IMenuService } from 'vs/platform/actions/common/actions';
import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands';
import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput';
import { IQuickPickItem, IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
@ -61,7 +59,7 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/
import { Codicon } from 'vs/base/common/codicons';
import { IViewsService } from 'vs/workbench/common/views';
import { IActionViewItemOptions, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { EXTENSIONS_CONFIG } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig';
import { EXTENSIONS_CONFIG, IExtensionsConfigContent } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig';
import { getErrorMessage, isPromiseCanceledError } from 'vs/base/common/errors';
import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
import { ActionWithDropdownActionViewItem, IActionWithDropdownActionViewItemOptions } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
@ -834,29 +832,37 @@ export class DropDownMenuActionViewItem extends ExtensionActionViewItem {
}
}
export function getContextMenuActions(menuService: IMenuService, contextKeyService: IContextKeyService, instantiationService: IInstantiationService, extension: IExtension | undefined | null): IAction[][] {
const scopedContextKeyService = contextKeyService.createScoped();
if (extension) {
scopedContextKeyService.createKey<string>('extension', extension.identifier.id);
scopedContextKeyService.createKey<boolean>('isBuiltinExtension', extension.isBuiltin);
scopedContextKeyService.createKey<boolean>('extensionHasConfiguration', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes.configuration);
if (extension.state === ExtensionState.Installed) {
scopedContextKeyService.createKey<string>('extensionStatus', 'installed');
export function getContextMenuActions(extension: IExtension | undefined | null, instantiationService: IInstantiationService): IAction[][] {
return instantiationService.invokeFunction(accessor => {
const scopedContextKeyService = accessor.get(IContextKeyService).createScoped();
const menuService = accessor.get(IMenuService);
const extensionRecommendationsService = accessor.get(IExtensionRecommendationsService);
const extensionIgnoredRecommendationsService = accessor.get(IExtensionIgnoredRecommendationsService);
if (extension) {
scopedContextKeyService.createKey<string>('extension', extension.identifier.id);
scopedContextKeyService.createKey<boolean>('isBuiltinExtension', extension.isBuiltin);
scopedContextKeyService.createKey<boolean>('extensionHasConfiguration', extension.local && !!extension.local.manifest.contributes && !!extension.local.manifest.contributes.configuration);
scopedContextKeyService.createKey<boolean>('isExtensionRecommended', !!extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]);
scopedContextKeyService.createKey<boolean>('isExtensionWorkspaceRecommended', extensionRecommendationsService.getAllRecommendationsWithReason()[extension.identifier.id.toLowerCase()]?.reasonId === ExtensionRecommendationReason.Workspace);
scopedContextKeyService.createKey<boolean>('isUserIgnoredRecommendation', extensionIgnoredRecommendationsService.globalIgnoredRecommendations.some(e => e === extension.identifier.id.toLowerCase()));
if (extension.state === ExtensionState.Installed) {
scopedContextKeyService.createKey<string>('extensionStatus', 'installed');
}
}
}
const groups: IAction[][] = [];
const menu = menuService.createMenu(MenuId.ExtensionContext, scopedContextKeyService);
menu.getActions({ shouldForwardArgs: true }).forEach(([, actions]) => groups.push(actions.map(action => {
if (action instanceof SubmenuAction) {
return action;
}
return instantiationService.createInstance(MenuItemExtensionAction, action);
})));
menu.dispose();
scopedContextKeyService.dispose();
const groups: IAction[][] = [];
const menu = menuService.createMenu(MenuId.ExtensionContext, scopedContextKeyService);
menu.getActions({ shouldForwardArgs: true }).forEach(([, actions]) => groups.push(actions.map(action => {
if (action instanceof SubmenuAction) {
return action;
}
return instantiationService.createInstance(MenuItemExtensionAction, action);
})));
menu.dispose();
scopedContextKeyService.dispose();
return groups;
return groups;
});
}
export class ManageExtensionAction extends ExtensionDropDownAction {
@ -870,8 +876,6 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
@IInstantiationService instantiationService: IInstantiationService,
@IExtensionService private readonly extensionService: IExtensionService,
@IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService,
@IMenuService private readonly menuService: IMenuService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
) {
super(ManageExtensionAction.ID, '', '', true, true, instantiationService);
@ -911,7 +915,7 @@ export class ManageExtensionAction extends ExtensionDropDownAction {
groups.push([this.instantiationService.createInstance(UninstallAction)]);
groups.push([this.instantiationService.createInstance(InstallAnotherVersionAction)]);
getContextMenuActions(this.menuService, this.contextKeyService, this.instantiationService, this.extension).forEach(actions => groups.push(actions));
getContextMenuActions(this.extension, this.instantiationService).forEach(actions => groups.push(actions));
groups.forEach(group => group.forEach(extensionAction => {
if (extensionAction instanceof ExtensionAction) {
@ -2134,124 +2138,6 @@ export class ChangeSortAction extends Action {
}
}
export class ConfigureRecommendedExtensionsCommandsContributor extends Disposable implements IWorkbenchContribution {
private workspaceContextKey = new RawContextKey<boolean>('workspaceRecommendations', true);
private workspaceFolderContextKey = new RawContextKey<boolean>('workspaceFolderRecommendations', true);
private addToWorkspaceRecommendationsContextKey = new RawContextKey<boolean>('addToWorkspaceRecommendations', false);
private addToWorkspaceFolderRecommendationsContextKey = new RawContextKey<boolean>('addToWorkspaceFolderRecommendations', false);
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
@IEditorService editorService: IEditorService
) {
super();
const boundWorkspaceContextKey = this.workspaceContextKey.bindTo(contextKeyService);
boundWorkspaceContextKey.set(workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE);
this._register(workspaceContextService.onDidChangeWorkbenchState(() => boundWorkspaceContextKey.set(workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE)));
const boundWorkspaceFolderContextKey = this.workspaceFolderContextKey.bindTo(contextKeyService);
boundWorkspaceFolderContextKey.set(workspaceContextService.getWorkspace().folders.length > 0);
this._register(workspaceContextService.onDidChangeWorkspaceFolders(() => boundWorkspaceFolderContextKey.set(workspaceContextService.getWorkspace().folders.length > 0)));
const boundAddToWorkspaceRecommendationsContextKey = this.addToWorkspaceRecommendationsContextKey.bindTo(contextKeyService);
boundAddToWorkspaceRecommendationsContextKey.set(editorService.activeEditor instanceof ExtensionsInput && workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE);
this._register(editorService.onDidActiveEditorChange(() => boundAddToWorkspaceRecommendationsContextKey.set(
editorService.activeEditor instanceof ExtensionsInput && workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE)));
this._register(workspaceContextService.onDidChangeWorkbenchState(() => boundAddToWorkspaceRecommendationsContextKey.set(
editorService.activeEditor instanceof ExtensionsInput && workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE)));
const boundAddToWorkspaceFolderRecommendationsContextKey = this.addToWorkspaceFolderRecommendationsContextKey.bindTo(contextKeyService);
boundAddToWorkspaceFolderRecommendationsContextKey.set(editorService.activeEditor instanceof ExtensionsInput);
this._register(editorService.onDidActiveEditorChange(() => boundAddToWorkspaceFolderRecommendationsContextKey.set(editorService.activeEditor instanceof ExtensionsInput)));
this.registerCommands();
}
private registerCommands(): void {
CommandsRegistry.registerCommand(ConfigureWorkspaceRecommendedExtensionsAction.ID, serviceAccessor => {
serviceAccessor.get(IInstantiationService).createInstance(ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceRecommendedExtensionsAction.ID, ConfigureWorkspaceRecommendedExtensionsAction.LABEL).run();
});
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: ConfigureWorkspaceRecommendedExtensionsAction.ID,
title: { value: ConfigureWorkspaceRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace)' },
category: localize('extensions', "Extensions")
},
when: this.workspaceContextKey
});
CommandsRegistry.registerCommand(ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, serviceAccessor => {
serviceAccessor.get(IInstantiationService).createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL).run();
});
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: ConfigureWorkspaceFolderRecommendedExtensionsAction.ID,
title: { value: ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL, original: 'Configure Recommended Extensions (Workspace Folder)' },
category: localize('extensions', "Extensions")
},
when: this.workspaceFolderContextKey
});
CommandsRegistry.registerCommand(AddToWorkspaceRecommendationsAction.ADD_ID, serviceAccessor => {
serviceAccessor.get(IInstantiationService)
.createInstance(AddToWorkspaceRecommendationsAction, AddToWorkspaceRecommendationsAction.ADD_ID, AddToWorkspaceRecommendationsAction.ADD_LABEL)
.run(AddToWorkspaceRecommendationsAction.ADD);
});
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: AddToWorkspaceRecommendationsAction.ADD_ID,
title: { value: AddToWorkspaceRecommendationsAction.ADD_LABEL, original: 'Add to Recommended Extensions (Workspace)' },
category: localize('extensions', "Extensions")
},
when: this.addToWorkspaceRecommendationsContextKey
});
CommandsRegistry.registerCommand(AddToWorkspaceFolderRecommendationsAction.ADD_ID, serviceAccessor => {
serviceAccessor.get(IInstantiationService)
.createInstance(AddToWorkspaceFolderRecommendationsAction, AddToWorkspaceFolderRecommendationsAction.ADD_ID, AddToWorkspaceFolderRecommendationsAction.ADD_LABEL)
.run(AddToWorkspaceRecommendationsAction.ADD);
});
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: AddToWorkspaceFolderRecommendationsAction.ADD_ID,
title: { value: AddToWorkspaceFolderRecommendationsAction.ADD_LABEL, original: 'Extensions: Add to Recommended Extensions (Workspace Folder)' },
category: localize('extensions', "Extensions")
},
when: this.addToWorkspaceFolderRecommendationsContextKey
});
CommandsRegistry.registerCommand(AddToWorkspaceRecommendationsAction.IGNORE_ID, serviceAccessor => {
serviceAccessor.get(IInstantiationService)
.createInstance(AddToWorkspaceRecommendationsAction, AddToWorkspaceRecommendationsAction.IGNORE_ID, AddToWorkspaceRecommendationsAction.IGNORE_LABEL)
.run(AddToWorkspaceRecommendationsAction.IGNORE);
});
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: AddToWorkspaceRecommendationsAction.IGNORE_ID,
title: { value: AddToWorkspaceRecommendationsAction.IGNORE_LABEL, original: 'Extensions: Ignore Recommended Extension (Workspace)' },
category: localize('extensions', "Extensions")
},
when: this.addToWorkspaceRecommendationsContextKey
});
CommandsRegistry.registerCommand(AddToWorkspaceFolderRecommendationsAction.IGNORE_ID, serviceAccessor => {
serviceAccessor.get(IInstantiationService)
.createInstance(AddToWorkspaceFolderRecommendationsAction, AddToWorkspaceFolderRecommendationsAction.IGNORE_ID, AddToWorkspaceFolderRecommendationsAction.IGNORE_LABEL)
.run(AddToWorkspaceRecommendationsAction.IGNORE);
});
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: AddToWorkspaceFolderRecommendationsAction.IGNORE_ID,
title: { value: AddToWorkspaceFolderRecommendationsAction.IGNORE_LABEL, original: 'Extensions: Ignore Recommended Extension (Workspace Folder)' },
category: localize('extensions', "Extensions")
},
when: this.addToWorkspaceFolderRecommendationsContextKey
});
}
}
export abstract class AbstractConfigureRecommendedExtensionsAction extends Action {
constructor(
@ -2293,83 +2179,6 @@ export abstract class AbstractConfigureRecommendedExtensionsAction extends Actio
}));
}
protected addExtensionToWorkspaceConfig(workspaceConfigurationFile: URI, extensionId: string, shouldRecommend: boolean) {
return this.getOrUpdateWorkspaceConfigurationFile(workspaceConfigurationFile)
.then(content => {
const extensionIdLowerCase = extensionId.toLowerCase();
const workspaceExtensionsConfigContent: IExtensionsConfigContent = (json.parse(content.value.toString()) || {})['extensions'] || {};
let insertInto = shouldRecommend ? workspaceExtensionsConfigContent.recommendations || [] : workspaceExtensionsConfigContent.unwantedRecommendations || [];
let removeFrom = shouldRecommend ? workspaceExtensionsConfigContent.unwantedRecommendations || [] : workspaceExtensionsConfigContent.recommendations || [];
if (insertInto.some(e => e.toLowerCase() === extensionIdLowerCase)) {
return Promise.resolve(null);
}
insertInto.push(extensionId);
removeFrom = removeFrom.filter(x => x.toLowerCase() !== extensionIdLowerCase);
return this.jsonEditingService.write(workspaceConfigurationFile,
[{
path: ['extensions'],
value: {
recommendations: shouldRecommend ? insertInto : removeFrom,
unwantedRecommendations: shouldRecommend ? removeFrom : insertInto
}
}],
true);
});
}
protected addExtensionToWorkspaceFolderConfig(extensionsFileResource: URI, extensionId: string, shouldRecommend: boolean): Promise<any> {
return this.getOrCreateExtensionsFile(extensionsFileResource)
.then(({ content }) => {
const extensionIdLowerCase = extensionId.toLowerCase();
const extensionsConfigContent: IExtensionsConfigContent = json.parse(content) || {};
let insertInto = shouldRecommend ? extensionsConfigContent.recommendations || [] : extensionsConfigContent.unwantedRecommendations || [];
let removeFrom = shouldRecommend ? extensionsConfigContent.unwantedRecommendations || [] : extensionsConfigContent.recommendations || [];
if (insertInto.some(e => e.toLowerCase() === extensionIdLowerCase)) {
return Promise.resolve(null);
}
insertInto.push(extensionId);
let removeFromPromise: Promise<void> = Promise.resolve();
if (removeFrom.some(e => e.toLowerCase() === extensionIdLowerCase)) {
removeFrom = removeFrom.filter(x => x.toLowerCase() !== extensionIdLowerCase);
removeFromPromise = this.jsonEditingService.write(extensionsFileResource,
[{
path: shouldRecommend ? ['unwantedRecommendations'] : ['recommendations'],
value: removeFrom
}],
true);
}
return removeFromPromise.then(() =>
this.jsonEditingService.write(extensionsFileResource,
[{
path: shouldRecommend ? ['recommendations'] : ['unwantedRecommendations'],
value: insertInto
}],
true)
);
});
}
protected getWorkspaceExtensionsConfigContent(extensionsFileResource: URI): Promise<IExtensionsConfigContent> {
return Promise.resolve(this.fileService.readFile(extensionsFileResource))
.then(content => {
return (json.parse(content.value.toString()) || {})['extensions'] || {};
}, err => ({ recommendations: [], unwantedRecommendations: [] }));
}
protected getWorkspaceFolderExtensionsConfigContent(extensionsFileResource: URI): Promise<IExtensionsConfigContent> {
return Promise.resolve(this.fileService.readFile(extensionsFileResource))
.then(content => {
return (<IExtensionsConfigContent>json.parse(content.value.toString()) || {});
}, err => ({ recommendations: [], unwantedRecommendations: [] }));
}
private getOrUpdateWorkspaceConfigurationFile(workspaceConfigurationFile: URI): Promise<IFileContent> {
return Promise.resolve(this.fileService.readFile(workspaceConfigurationFile))
.then(content => {
@ -2488,161 +2297,6 @@ export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends Abstrac
}
}
export class AddToWorkspaceFolderRecommendationsAction extends AbstractConfigureRecommendedExtensionsAction {
static readonly ADD = true;
static readonly IGNORE = false;
static readonly ADD_ID = 'workbench.extensions.action.addToWorkspaceFolderRecommendations';
static readonly ADD_LABEL = localize('addToWorkspaceFolderRecommendations', "Add to Recommended Extensions (Workspace Folder)");
static readonly IGNORE_ID = 'workbench.extensions.action.addToWorkspaceFolderIgnoredRecommendations';
static readonly IGNORE_LABEL = localize('addToWorkspaceFolderIgnoredRecommendations', "Ignore Recommended Extension (Workspace Folder)");
constructor(
id: string,
label: string,
@IFileService fileService: IFileService,
@ITextFileService textFileService: ITextFileService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IEditorService editorService: IEditorService,
@IJSONEditingService jsonEditingService: IJSONEditingService,
@ITextModelService textModelResolverService: ITextModelService,
@ICommandService private readonly commandService: ICommandService,
@INotificationService private readonly notificationService: INotificationService
) {
super(id, label, contextService, fileService, textFileService, editorService, jsonEditingService, textModelResolverService);
}
run(shouldRecommend: boolean): Promise<void> {
if (!(this.editorService.activeEditor instanceof ExtensionsInput) || !this.editorService.activeEditor.extension) {
return Promise.resolve();
}
const folders = this.contextService.getWorkspace().folders;
if (!folders || !folders.length) {
this.notificationService.info(localize('AddToWorkspaceFolderRecommendations.noWorkspace', 'There are no workspace folders open to add recommendations.'));
return Promise.resolve();
}
const extensionId = this.editorService.activeEditor.extension.identifier;
const pickFolderPromise = folders.length === 1
? Promise.resolve(folders[0])
: this.commandService.executeCommand<IWorkspaceFolder>(PICK_WORKSPACE_FOLDER_COMMAND_ID);
return Promise.resolve(pickFolderPromise)
.then(workspaceFolder => {
if (!workspaceFolder) {
return Promise.resolve();
}
const configurationFile = workspaceFolder.toResource(EXTENSIONS_CONFIG);
return this.getWorkspaceFolderExtensionsConfigContent(configurationFile).then(content => {
const extensionIdLowerCase = extensionId.id.toLowerCase();
if (shouldRecommend) {
if ((content.recommendations || []).some(e => e.toLowerCase() === extensionIdLowerCase)) {
this.notificationService.info(localize('AddToWorkspaceFolderRecommendations.alreadyExists', 'This extension is already present in this workspace folder\'s recommendations.'));
return Promise.resolve();
}
return this.addExtensionToWorkspaceFolderConfig(configurationFile, extensionId.id, shouldRecommend).then(() => {
this.notificationService.prompt(Severity.Info,
localize('AddToWorkspaceFolderRecommendations.success', 'The extension was successfully added to this workspace folder\'s recommendations.'),
[{
label: localize('viewChanges', "View Changes"),
run: () => this.openExtensionsFile(configurationFile)
}]);
}, err => {
this.notificationService.error(localize('AddToWorkspaceFolderRecommendations.failure', 'Failed to write to extensions.json. {0}', err));
});
}
else {
if ((content.unwantedRecommendations || []).some(e => e.toLowerCase() === extensionIdLowerCase)) {
this.notificationService.info(localize('AddToWorkspaceFolderIgnoredRecommendations.alreadyExists', 'This extension is already present in this workspace folder\'s unwanted recommendations.'));
return Promise.resolve();
}
return this.addExtensionToWorkspaceFolderConfig(configurationFile, extensionId.id, shouldRecommend).then(() => {
this.notificationService.prompt(Severity.Info,
localize('AddToWorkspaceFolderIgnoredRecommendations.success', 'The extension was successfully added to this workspace folder\'s unwanted recommendations.'),
[{
label: localize('viewChanges', "View Changes"),
run: () => this.openExtensionsFile(configurationFile)
}]);
}, err => {
this.notificationService.error(localize('AddToWorkspaceFolderRecommendations.failure', 'Failed to write to extensions.json. {0}', err));
});
}
});
});
}
}
export class AddToWorkspaceRecommendationsAction extends AbstractConfigureRecommendedExtensionsAction {
static readonly ADD = true;
static readonly IGNORE = false;
static readonly ADD_ID = 'workbench.extensions.action.addToWorkspaceRecommendations';
static readonly ADD_LABEL = localize('addToWorkspaceRecommendations', "Add to Recommended Extensions (Workspace)");
static readonly IGNORE_ID = 'workbench.extensions.action.addToWorkspaceIgnoredRecommendations';
static readonly IGNORE_LABEL = localize('addToWorkspaceIgnoredRecommendations', "Ignore Recommended Extension (Workspace)");
constructor(
id: string,
label: string,
@IFileService fileService: IFileService,
@ITextFileService textFileService: ITextFileService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IEditorService editorService: IEditorService,
@IJSONEditingService jsonEditingService: IJSONEditingService,
@ITextModelService textModelResolverService: ITextModelService,
@INotificationService private readonly notificationService: INotificationService
) {
super(id, label, contextService, fileService, textFileService, editorService, jsonEditingService, textModelResolverService);
}
run(shouldRecommend: boolean): Promise<void> {
const workspaceConfig = this.contextService.getWorkspace().configuration;
if (!(this.editorService.activeEditor instanceof ExtensionsInput) || !this.editorService.activeEditor.extension || !workspaceConfig) {
return Promise.resolve();
}
const extensionId = this.editorService.activeEditor.extension.identifier;
return this.getWorkspaceExtensionsConfigContent(workspaceConfig).then(content => {
const extensionIdLowerCase = extensionId.id.toLowerCase();
if (shouldRecommend) {
if ((content.recommendations || []).some(e => e.toLowerCase() === extensionIdLowerCase)) {
this.notificationService.info(localize('AddToWorkspaceRecommendations.alreadyExists', 'This extension is already present in workspace recommendations.'));
return Promise.resolve();
}
return this.addExtensionToWorkspaceConfig(workspaceConfig, extensionId.id, shouldRecommend).then(() => {
this.notificationService.prompt(Severity.Info,
localize('AddToWorkspaceRecommendations.success', 'The extension was successfully added to this workspace\'s recommendations.'),
[{
label: localize('viewChanges', "View Changes"),
run: () => this.openWorkspaceConfigurationFile(workspaceConfig)
}]);
}, err => {
this.notificationService.error(localize('AddToWorkspaceRecommendations.failure', 'Failed to write. {0}', err));
});
} else {
if ((content.unwantedRecommendations || []).some(e => e.toLowerCase() === extensionIdLowerCase)) {
this.notificationService.info(localize('AddToWorkspaceUnwantedRecommendations.alreadyExists', 'This extension is already present in workspace unwanted recommendations.'));
return Promise.resolve();
}
return this.addExtensionToWorkspaceConfig(workspaceConfig, extensionId.id, shouldRecommend).then(() => {
this.notificationService.prompt(Severity.Info,
localize('AddToWorkspaceUnwantedRecommendations.success', 'The extension was successfully added to this workspace\'s unwanted recommendations.'),
[{
label: localize('viewChanges', "View Changes"),
run: () => this.openWorkspaceConfigurationFile(workspaceConfig)
}]);
}, err => {
this.notificationService.error(localize('AddToWorkspaceRecommendations.failure', 'Failed to write. {0}', err));
});
}
});
}
}
export class StatusLabelAction extends Action implements IExtensionContainer {
private static readonly ENABLED_CLASS = `${ExtensionAction.TEXT_ACTION_CLASS} extension-status-label`;

View file

@ -44,7 +44,6 @@ import { IProductService } from 'vs/platform/product/common/productService';
import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { IMenuService } from 'vs/platform/actions/common/actions';
import { IViewDescriptorService } from 'vs/workbench/common/views';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences';
@ -112,7 +111,6 @@ export class ExtensionsListView extends ViewPane {
@IProductService protected readonly productService: IProductService,
@IContextKeyService contextKeyService: IContextKeyService,
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
@IMenuService private readonly menuService: IMenuService,
@IOpenerService openerService: IOpenerService,
@IPreferencesService private readonly preferencesService: IPreferencesService,
) {
@ -251,7 +249,7 @@ export class ExtensionsListView extends ViewPane {
getActions: () => actions.slice(0, actions.length - 1)
});
} else if (e.element) {
const groups = getContextMenuActions(this.menuService, this.contextKeyService.createScoped(), this.instantiationService, e.element);
const groups = getContextMenuActions(e.element, this.instantiationService);
groups.forEach(group => group.forEach(extensionAction => {
if (extensionAction instanceof ExtensionAction) {
extensionAction.extension = e.element!;
@ -890,7 +888,6 @@ export class ServerExtensionsView extends ExtensionsListView {
@IWorkbenchExtensioManagementService extensionManagementService: IWorkbenchExtensioManagementService,
@IProductService productService: IProductService,
@IContextKeyService contextKeyService: IContextKeyService,
@IMenuService menuService: IMenuService,
@IOpenerService openerService: IOpenerService,
@IThemeService themeService: IThemeService,
@IPreferencesService preferencesService: IPreferencesService,
@ -898,7 +895,7 @@ export class ServerExtensionsView extends ExtensionsListView {
options.server = server;
super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService,
telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, extensionManagementService, productService,
contextKeyService, viewDescriptorService, menuService, openerService, preferencesService);
contextKeyService, viewDescriptorService, openerService, preferencesService);
this._register(onDidChangeTitle(title => this.updateTitle(title)));
}

View file

@ -7,12 +7,12 @@ import { EXTENSION_IDENTIFIER_PATTERN, IExtensionGalleryService } from 'vs/platf
import { distinct, flatten } from 'vs/base/common/arrays';
import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IExtensionsConfigContent, ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionRecommendations/common/extensionRecommendations';
import { ILogService } from 'vs/platform/log/common/log';
import { CancellationToken } from 'vs/base/common/cancellation';
import { localize } from 'vs/nls';
import { Emitter } from 'vs/base/common/event';
import { IWorkpsaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig';
import { IExtensionsConfigContent, IWorkpsaceExtensionsConfigService } from 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig';
export class WorkspaceRecommendations extends ExtensionRecommendations {
@ -51,23 +51,28 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
this.notificationService.warn(`The ${invalidRecommendations.length} extension(s) below, in workspace recommendations have issues:\n${message}`);
}
this._recommendations = [];
this._ignoredRecommendations = [];
for (const extensionsConfig of extensionsConfigs) {
for (const unwantedRecommendation of extensionsConfig.unwantedRecommendations) {
if (invalidRecommendations.indexOf(unwantedRecommendation) === -1) {
this._ignoredRecommendations.push(unwantedRecommendation);
if (extensionsConfig.unwantedRecommendations) {
for (const unwantedRecommendation of extensionsConfig.unwantedRecommendations) {
if (invalidRecommendations.indexOf(unwantedRecommendation) === -1) {
this._ignoredRecommendations.push(unwantedRecommendation);
}
}
}
for (const extensionId of extensionsConfig.recommendations) {
if (invalidRecommendations.indexOf(extensionId) === -1) {
this._recommendations.push({
extensionId,
reason: {
reasonId: ExtensionRecommendationReason.Workspace,
reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.")
}
});
if (extensionsConfig.recommendations) {
for (const extensionId of extensionsConfig.recommendations) {
if (invalidRecommendations.indexOf(extensionId) === -1) {
this._recommendations.push({
extensionId,
reason: {
reasonId: ExtensionRecommendationReason.Workspace,
reasonText: localize('workspaceRecommendation', "This extension is recommended by users of the current workspace.")
}
});
}
}
}
}
@ -114,12 +119,8 @@ export class WorkspaceRecommendations extends ExtensionRecommendations {
}
private async onDidChangeExtensionsConfigs(): Promise<void> {
const oldWorkspaceRecommended = this._recommendations;
await this.fetch();
// Suggest only if at least one of the newly added recommendations was not suggested before
if (this._recommendations.some(current => oldWorkspaceRecommended.every(old => current.extensionId !== old.extensionId))) {
this._onDidChangeRecommendations.fire();
}
this._onDidChangeRecommendations.fire();
}
}

View file

@ -304,14 +304,14 @@ suite('ExtensionRecommendationsService Test', () => {
}, null, '\t'));
const myWorkspace = testWorkspace(URI.from({ scheme: 'file', path: folderDir }));
const fileService = new FileService(new NullLogService());
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
instantiationService.stub(IFileService, fileService);
workspaceService = new TestContextService(myWorkspace);
instantiationService.stub(IWorkspaceContextService, workspaceService);
instantiationService.stub(IWorkpsaceExtensionsConfigService, instantiationService.createInstance(WorkspaceExtensionsConfigService));
instantiationService.stub(IExtensionIgnoredRecommendationsService, instantiationService.createInstance(ExtensionIgnoredRecommendationsService));
instantiationService.stub(IExtensionRecommendationNotificationService, instantiationService.createInstance(ExtensionRecommendationNotificationService));
const fileService = new FileService(new NullLogService());
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
instantiationService.stub(IFileService, fileService);
}
function testNoPromptForValidRecommendations(recommendations: string[]) {

View file

@ -7,11 +7,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { IStringDictionary } from 'vs/base/common/collections';
import { Event } from 'vs/base/common/event';
export interface IExtensionsConfigContent {
recommendations: string[];
unwantedRecommendations: string[];
}
export type DynamicRecommendation = 'dynamic';
export type ConfigRecommendation = 'config';
export type ExecutableRecommendation = 'executable';

View file

@ -3,20 +3,28 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { coalesce, distinct, flatten } from 'vs/base/common/arrays';
import { distinct, flatten } from 'vs/base/common/arrays';
import { Emitter, Event } from 'vs/base/common/event';
import { parse } from 'vs/base/common/json';
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService } from 'vs/platform/files/common/files';
import { getIconClasses } from 'vs/editor/common/services/getIconClasses';
import { FileKind, IFileService } from 'vs/platform/files/common/files';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { localize } from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import { IJSONEditingService, IJSONValue } from 'vs/workbench/services/configuration/common/jsonEditing';
import { ResourceMap } from 'vs/base/common/map';
export const EXTENSIONS_CONFIG = '.vscode/extensions.json';
export interface IExtensionsConfigContent {
recommendations: string[];
unwantedRecommendations: string[];
recommendations?: string[];
unwantedRecommendations?: string[];
}
export const IWorkpsaceExtensionsConfigService = createDecorator<IWorkpsaceExtensionsConfigService>('IWorkpsaceExtensionsConfigService');
@ -26,8 +34,11 @@ export interface IWorkpsaceExtensionsConfigService {
onDidChangeExtensionsConfigs: Event<void>;
getExtensionsConfigs(): Promise<IExtensionsConfigContent[]>;
getRecommendations(): Promise<string[]>;
getUnwantedRecommendations(): Promise<string[]>;
toggleRecommendation(extensionId: string): Promise<void>;
toggleUnwantedRecommendation(extensionId: string): Promise<void>;
}
export class WorkspaceExtensionsConfigService extends Disposable implements IWorkpsaceExtensionsConfigService {
@ -40,53 +51,217 @@ export class WorkspaceExtensionsConfigService extends Disposable implements IWor
constructor(
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IFileService private readonly fileService: IFileService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IModelService private readonly modelService: IModelService,
@IModeService private readonly modeService: IModeService,
@IJSONEditingService private readonly jsonEditingService: IJSONEditingService,
) {
super();
this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(e => this._onDidChangeExtensionsConfigs.fire()));
this._register(workspaceContextService.onDidChangeWorkspaceFolders(e => this._onDidChangeExtensionsConfigs.fire()));
this._register(fileService.onDidFilesChange(e => {
const workspace = workspaceContextService.getWorkspace();
if ((workspace.configuration && e.affects(workspace.configuration))
|| workspace.folders.some(folder => e.affects(folder.toResource(EXTENSIONS_CONFIG)))
) {
this._onDidChangeExtensionsConfigs.fire();
}
}));
}
async getExtensionsConfigs(): Promise<IExtensionsConfigContent[]> {
const workspace = this.workspaceContextService.getWorkspace();
const result = await Promise.all([
this.resolveWorkspaceExtensionConfig(workspace),
...workspace.folders.map(workspaceFolder => this.resolveWorkspaceFolderExtensionConfig(workspaceFolder))
]);
return coalesce(result);
const result: IExtensionsConfigContent[] = [];
const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined;
if (workspaceExtensionsConfigContent) {
result.push(workspaceExtensionsConfigContent);
}
result.push(...await Promise.all(workspace.folders.map(workspaceFolder => this.resolveWorkspaceFolderExtensionConfig(workspaceFolder))));
return result;
}
async getRecommendations(): Promise<string[]> {
const configs = await this.getExtensionsConfigs();
return distinct(flatten(configs.map(c => c.recommendations ? c.recommendations.map(c => c.toLowerCase()) : [])));
}
async getUnwantedRecommendations(): Promise<string[]> {
const configs = await this.getExtensionsConfigs();
return distinct(flatten(configs.map(c => c.unwantedRecommendations.map(c => c.toLowerCase()))));
return distinct(flatten(configs.map(c => c.unwantedRecommendations ? c.unwantedRecommendations.map(c => c.toLowerCase()) : [])));
}
private async resolveWorkspaceExtensionConfig(workspace: IWorkspace): Promise<IExtensionsConfigContent | null> {
try {
if (workspace.configuration) {
const content = await this.fileService.readFile(workspace.configuration);
const extensionsConfigContent = <IExtensionsConfigContent | undefined>parse(content.value.toString())['extensions'];
return this.parseExtensionConfig(extensionsConfigContent);
async toggleRecommendation(extensionId: string): Promise<void> {
const workspace = this.workspaceContextService.getWorkspace();
const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined;
const workspaceFolderExtensionsConfigContents = new ResourceMap<IExtensionsConfigContent>();
await Promise.all(workspace.folders.map(async workspaceFolder => {
const extensionsConfigContent = await this.resolveWorkspaceFolderExtensionConfig(workspaceFolder);
workspaceFolderExtensionsConfigContents.set(workspaceFolder.uri, extensionsConfigContent);
}));
const isWorkspaceRecommended = workspaceExtensionsConfigContent && workspaceExtensionsConfigContent.recommendations?.some(r => r === extensionId);
const recommendedWorksapceFolders = workspace.folders.filter(workspaceFolder => workspaceFolderExtensionsConfigContents.get(workspaceFolder.uri)?.recommendations?.some(r => r === extensionId));
const isRecommended = isWorkspaceRecommended || recommendedWorksapceFolders.length > 0;
const workspaceOrFolders = isRecommended
? await this.pickWorkspaceOrFolders(recommendedWorksapceFolders, isWorkspaceRecommended ? workspace : undefined, localize('select for remove', "Remove extension recommendation from"))
: await this.pickWorkspaceOrFolders(workspace.folders, workspace.configuration ? workspace : undefined, localize('select for add', "Add extension recommendation to"));
for (const workspaceOrWorkspaceFolder of workspaceOrFolders) {
if (IWorkspace.isIWorkspace(workspaceOrWorkspaceFolder)) {
await this.addOrRemoveWorkspaceRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceExtensionsConfigContent, !isRecommended);
} else {
await this.addOrRemoveWorkspaceFolderRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceFolderExtensionsConfigContents.get(workspaceOrWorkspaceFolder.uri)!, !isRecommended);
}
} catch (e) { /* Ignore */ }
return null;
}
}
private async resolveWorkspaceFolderExtensionConfig(workspaceFolder: IWorkspaceFolder): Promise<IExtensionsConfigContent | null> {
async toggleUnwantedRecommendation(extensionId: string): Promise<void> {
const workspace = this.workspaceContextService.getWorkspace();
const workspaceExtensionsConfigContent = workspace.configuration ? await this.resolveWorkspaceExtensionConfig(workspace.configuration) : undefined;
const workspaceFolderExtensionsConfigContents = new ResourceMap<IExtensionsConfigContent>();
await Promise.all(workspace.folders.map(async workspaceFolder => {
const extensionsConfigContent = await this.resolveWorkspaceFolderExtensionConfig(workspaceFolder);
workspaceFolderExtensionsConfigContents.set(workspaceFolder.uri, extensionsConfigContent);
}));
const isWorkspaceUnwanted = workspaceExtensionsConfigContent && workspaceExtensionsConfigContent.unwantedRecommendations?.some(r => r === extensionId);
const unWantedWorksapceFolders = workspace.folders.filter(workspaceFolder => workspaceFolderExtensionsConfigContents.get(workspaceFolder.uri)?.unwantedRecommendations?.some(r => r === extensionId));
const isUnwanted = isWorkspaceUnwanted || unWantedWorksapceFolders.length > 0;
const workspaceOrFolders = isUnwanted
? await this.pickWorkspaceOrFolders(unWantedWorksapceFolders, isWorkspaceUnwanted ? workspace : undefined, localize('select for remove', "Remove extension recommendation from"))
: await this.pickWorkspaceOrFolders(workspace.folders, workspace.configuration ? workspace : undefined, localize('select for add', "Add extension recommendation to"));
for (const workspaceOrWorkspaceFolder of workspaceOrFolders) {
if (IWorkspace.isIWorkspace(workspaceOrWorkspaceFolder)) {
await this.addOrRemoveWorkspaceUnwantedRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceExtensionsConfigContent, !isUnwanted);
} else {
await this.addOrRemoveWorkspaceFolderUnwantedRecommendation(extensionId, workspaceOrWorkspaceFolder, workspaceFolderExtensionsConfigContents.get(workspaceOrWorkspaceFolder.uri)!, !isUnwanted);
}
}
}
private async addOrRemoveWorkspaceFolderRecommendation(extensionId: string, workspaceFolder: IWorkspaceFolder, extensionsConfigContent: IExtensionsConfigContent, add: boolean): Promise<void> {
const values: IJSONValue[] = [];
if (add) {
values.push({ path: ['recommendations'], value: [...extensionsConfigContent.recommendations || [], extensionId] });
if (extensionsConfigContent.unwantedRecommendations && extensionsConfigContent.unwantedRecommendations.some(e => e === extensionId)) {
values.push({ path: ['unwantedRecommendations'], value: extensionsConfigContent.unwantedRecommendations.filter(e => e !== extensionId) });
}
} else if (extensionsConfigContent.recommendations) {
values.push({ path: ['recommendations'], value: extensionsConfigContent.recommendations.filter(e => e !== extensionId) });
}
if (values.length) {
return this.jsonEditingService.write(workspaceFolder.toResource(EXTENSIONS_CONFIG), values, true);
}
}
private async addOrRemoveWorkspaceRecommendation(extensionId: string, workspace: IWorkspace, extensionsConfigContent: IExtensionsConfigContent | undefined, add: boolean): Promise<void> {
const values: IJSONValue[] = [];
if (extensionsConfigContent) {
if (add) {
values.push({ path: ['recommendations'], value: [...extensionsConfigContent.recommendations || [], extensionId] });
if (extensionsConfigContent.unwantedRecommendations && extensionsConfigContent.unwantedRecommendations.some(e => e === extensionId)) {
values.push({ path: ['unwantedRecommendations'], value: extensionsConfigContent.unwantedRecommendations.filter(e => e !== extensionId) });
}
} else if (extensionsConfigContent.recommendations) {
values.push({ path: ['recommendations'], value: extensionsConfigContent.recommendations.filter(e => e !== extensionId) });
}
} else if (add) {
values.push({ path: ['extensions'], value: { recommendations: [extensionId] } });
}
if (values.length) {
return this.jsonEditingService.write(workspace.configuration!, values, true);
}
}
private async addOrRemoveWorkspaceFolderUnwantedRecommendation(extensionId: string, workspaceFolder: IWorkspaceFolder, extensionsConfigContent: IExtensionsConfigContent, add: boolean): Promise<void> {
const values: IJSONValue[] = [];
if (add) {
values.push({ path: ['unwantedRecommendations'], value: [...extensionsConfigContent.unwantedRecommendations || [], extensionId] });
if (extensionsConfigContent.recommendations && extensionsConfigContent.recommendations.some(e => e === extensionId)) {
values.push({ path: ['recommendations'], value: extensionsConfigContent.recommendations.filter(e => e !== extensionId) });
}
} else if (extensionsConfigContent.unwantedRecommendations) {
values.push({ path: ['unwantedRecommendations'], value: extensionsConfigContent.unwantedRecommendations.filter(e => e !== extensionId) });
}
if (values.length) {
return this.jsonEditingService.write(workspaceFolder.toResource(EXTENSIONS_CONFIG), values, true);
}
}
private async addOrRemoveWorkspaceUnwantedRecommendation(extensionId: string, workspace: IWorkspace, extensionsConfigContent: IExtensionsConfigContent | undefined, add: boolean): Promise<void> {
const values: IJSONValue[] = [];
if (extensionsConfigContent) {
if (add) {
values.push({ path: ['unwantedRecommendations'], value: [...extensionsConfigContent.unwantedRecommendations || [], extensionId] });
if (extensionsConfigContent.recommendations && extensionsConfigContent.recommendations.some(e => e === extensionId)) {
values.push({ path: ['recommendations'], value: extensionsConfigContent.recommendations.filter(e => e !== extensionId) });
}
} else if (extensionsConfigContent.unwantedRecommendations) {
values.push({ path: ['unwantedRecommendations'], value: extensionsConfigContent.unwantedRecommendations.filter(e => e !== extensionId) });
}
} else if (add) {
values.push({ path: ['extensions'], value: { unwantedRecommendations: [extensionId] } });
}
if (values.length) {
return this.jsonEditingService.write(workspace.configuration!, values, true);
}
}
private async pickWorkspaceOrFolders(workspaceFolders: IWorkspaceFolder[], workspace: IWorkspace | undefined, placeHolder: string): Promise<(IWorkspace | IWorkspaceFolder)[]> {
const workspaceOrFolders = workspace ? [...workspaceFolders, workspace] : [...workspaceFolders];
if (workspaceOrFolders.length === 1) {
return workspaceOrFolders;
}
const folderPicks: (IQuickPickItem & { workspaceOrFolder: IWorkspace | IWorkspaceFolder } | IQuickPickSeparator)[] = workspaceFolders.map(workspaceFolder => {
return {
label: workspaceFolder.name,
description: localize('workspace folder', "Workspace Folder"),
workspaceOrFolder: workspaceFolder,
iconClasses: getIconClasses(this.modelService, this.modeService, workspaceFolder.uri, FileKind.ROOT_FOLDER)
};
});
if (workspace) {
folderPicks.push({ type: 'separator' });
folderPicks.push({
label: localize('workspace', "Workspace"),
workspaceOrFolder: workspace,
});
}
const result = await this.quickInputService.pick(folderPicks, { placeHolder, canPickMany: true }) || [];
return result.map(r => r.workspaceOrFolder!);
}
private async resolveWorkspaceExtensionConfig(workspaceConfigurationResource: URI): Promise<IExtensionsConfigContent | undefined> {
try {
const content = await this.fileService.readFile(workspaceConfigurationResource);
const extensionsConfigContent = <IExtensionsConfigContent | undefined>parse(content.value.toString())['extensions'];
return extensionsConfigContent ? this.parseExtensionConfig(extensionsConfigContent) : undefined;
} catch (e) { /* Ignore */ }
return undefined;
}
private async resolveWorkspaceFolderExtensionConfig(workspaceFolder: IWorkspaceFolder): Promise<IExtensionsConfigContent> {
try {
const content = await this.fileService.readFile(workspaceFolder.toResource(EXTENSIONS_CONFIG));
const extensionsConfigContent = <IExtensionsConfigContent>parse(content.value.toString());
return this.parseExtensionConfig(extensionsConfigContent);
} catch (e) { /* ignore */ }
return null;
return {};
}
private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent | undefined): IExtensionsConfigContent | null {
if (extensionsConfigContent) {
return {
recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())),
unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase()))
};
}
return null;
private parseExtensionConfig(extensionsConfigContent: IExtensionsConfigContent): IExtensionsConfigContent {
return {
recommendations: distinct((extensionsConfigContent.recommendations || []).map(e => e.toLowerCase())),
unwantedRecommendations: distinct((extensionsConfigContent.unwantedRecommendations || []).map(e => e.toLowerCase()))
};
}
}