Skip to content

Commit

Permalink
Add "Recommend" action to extension viewlet (#50419)
Browse files Browse the repository at this point in the history
* Add "Recommend" action to extension viewlet

Related to #13456

* Address PR requested changes

* Change functionality and UX

- Button goes from "Add to workspace recommendations" (clickable) to
  "Adding to workspace recommendations..." (unclickable) to
  "Added to workspace recommendations" (unclickable).
- It is only visible if the current extension is installed and there's
  at least one folder in the root that doesn't have it recommended yet.
  In other words, if it's already recommended in every folder of the root,
  it is not visible.
- In a single-root setup, it's immediately added to the recommendations.
- In a multi-root setup, a quick-pick is displayed with only the folders
  where it is not yet recommended available as options.
- In a multi-root setup, the button will go back to
  "Add to workspace recommendations" (clickable) instead of the
  unclickable state if there are still folders remaining where the
  current extension is not yet recommended.
- An error or success notification is displayed after the work is done.
- Configuration files are created if don't exist, modified otherwise.

* Don't show the button if ext is recommended in ANY folder

In the previous commit, the button would be displayed as long as there
was at least one folder in the workspace where the extension wasn't
recommended.

Now, the button will be displayed only if the current extension is not
recommended in any of the folders, as suggested in
#50419 (comment)

* Improvements based on PR suggestions

- Lowercasing the extension ID before comparing
- Directly styling & labeling the button on the `run` command after work is done instead of calling `update`.
- Fix & delete unnecessary styles

* Expose feature as command instead of button

After a discussion with @ramya-rao-a and other members of the team,
it was decided that it wasn't reasonable to have a dedicated button for
a feature that's rarely used in such a visible place.

So instead, the action will be exposed as a command that will only be
available when there's an extension open in the editor area. In contrast
with the previous implementation, this has the added benefit of
allowing the user to use this action to recommend uninstalled
extensions as well.

* Simplify conditions for AddToRecommendations action

* Move comments so that they dont get erased when recommendations are added programatically

* Remove from unwanted before adding to recommendations

* Array improvements

* Friendly error msg
  • Loading branch information
reyronald authored and ramya-rao-a committed Jun 24, 2018
1 parent ba69c69 commit 5eea6bc
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,15 @@ export const ExtensionsConfigurationSchema: IJSONSchema = {

export const ExtensionsConfigurationInitialContent: string = [
'{',
'\t// See http://go.microsoft.com/fwlink/?LinkId=827846',
'\t// for the documentation about the extensions.json format',
'\t// See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.',
'\t// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp',
'',
'\t// List of extensions which should be recommended for users of this workspace.',
'\t"recommendations": [',
'\t\t// List of extensions which should be recommended for users of this workspace.',
'\t\t// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp',
'\t\t',
'\t],',
'\t// List of extensions that will be skipped from the recommendations that VS Code makes for the users of this workspace. These are extensions that you may consider to be irrelevant, redundant, or otherwise unwanted.',
'\t"unwantedRecommendations": [',
'\t\t// List of extensions that will be skipped from the recommendations that VS Code makes for the users of this workspace. These are extensions that you may consider to be irrelevant, redundant, or otherwise unwanted.',
'\t\t// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp',
'\t\t',
'\t]',
'}'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewlet, AutoUpdateConfigurationKey } from 'vs/workbench/parts/extensions/common/extensions';
import { ExtensionsConfigurationInitialContent } from 'vs/workbench/parts/extensions/common/extensionsFileTemplate';
import { LocalExtensionType, IExtensionEnablementService, IExtensionTipsService, EnablementState, ExtensionsLabel, IExtensionManagementServer, IExtensionManagementServerService, IGalleryExtension, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { LocalExtensionType, IExtensionEnablementService, IExtensionTipsService, EnablementState, ExtensionsLabel, IExtensionManagementServer, IExtensionManagementServerService, IGalleryExtension, ILocalExtension, IExtensionsConfigContent } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ToggleViewletAction } from 'vs/workbench/browser/viewlet';
Expand Down Expand Up @@ -49,6 +49,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
import { IQuickOpenService, IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
import { ExtensionsInput } from 'vs/workbench/parts/extensions/common/extensionsInput';
import product from 'vs/platform/node/product';

const promptDownloadManually = (extension: IGalleryExtension, message: string, instantiationService: IInstantiationService, notificationService: INotificationService, openerService: IOpenerService) => {
Expand Down Expand Up @@ -1709,21 +1710,26 @@ export class ConfigureRecommendedExtensionsCommandsContributor extends Disposabl

private workspaceContextKey = new RawContextKey<boolean>('workspaceRecommendations', true);
private workspaceFolderContextKey = new RawContextKey<boolean>('workspaceFolderRecommendations', true);
private addToRecommendationsContextKey = new RawContextKey<boolean>('addToRecommendations', false);

constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService
@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 boundAddToRecommendationsContextKey = this.addToRecommendationsContextKey.bindTo(contextKeyService);
boundAddToRecommendationsContextKey.set(editorService.activeEditor instanceof ExtensionsInput);
this._register(editorService.onDidActiveEditorChange(() => boundAddToRecommendationsContextKey.set(editorService.activeEditor instanceof ExtensionsInput)));

this.registerCommands();
}

Expand All @@ -1749,7 +1755,19 @@ export class ConfigureRecommendedExtensionsCommandsContributor extends Disposabl
},
when: this.workspaceFolderContextKey
});

CommandsRegistry.registerCommand(AddToWorkspaceRecommendationsAction.ID, serviceAccesor => {
serviceAccesor.get(IInstantiationService).createInstance(AddToWorkspaceRecommendationsAction, AddToWorkspaceRecommendationsAction.ID, AddToWorkspaceRecommendationsAction.LABEL).run();
});
MenuRegistry.appendMenuItem(MenuId.CommandPalette, {
command: {
id: AddToWorkspaceRecommendationsAction.ID,
title: `${ExtensionsLabel}: ${AddToWorkspaceRecommendationsAction.LABEL}`
},
when: this.addToRecommendationsContextKey
});
}

}

interface IExtensionsContent {
Expand All @@ -1763,7 +1781,7 @@ export abstract class AbstractConfigureRecommendedExtensionsAction extends Actio
label: string,
@IWorkspaceContextService protected contextService: IWorkspaceContextService,
@IFileService private fileService: IFileService,
@IEditorService private editorService: IEditorService,
@IEditorService protected editorService: IEditorService,
@IJSONEditingService private jsonEditingService: IJSONEditingService,
@ITextModelService private textModelResolverService: ITextModelService
) {
Expand Down Expand Up @@ -1797,6 +1815,57 @@ export abstract class AbstractConfigureRecommendedExtensionsAction extends Actio
}));
}

protected addRecommendedExtensionToFolder(extensionsFileResource: URI, extensionId: string): TPromise<any> {
return this.getOrCreateExtensionsFile(extensionsFileResource)
.then(({ content }) => {
const extensionIdLowerCase = extensionId.toLowerCase();
const jsonContent: IExtensionsConfigContent = json.parse(content) || {};
const folderRecommendations = jsonContent.recommendations || [];

if (folderRecommendations.some(e => e.toLowerCase() === extensionIdLowerCase)) {
return TPromise.as(null);
}
folderRecommendations.push(extensionId);

const folderUnwantedRecommedations = jsonContent.unwantedRecommendations || [];
let index = -1;
for (let i = 0; i < folderUnwantedRecommedations.length; i++) {
if (folderUnwantedRecommedations[i].toLowerCase() === extensionIdLowerCase) {
index = i;
break;
}
}

let removeFromUnwantedPromise = TPromise.wrap(null);
if (index > -1) {
folderUnwantedRecommedations.splice(index, 1);
removeFromUnwantedPromise = this.jsonEditingService.write(extensionsFileResource,
{
key: 'unwantedRecommendations',
value: folderUnwantedRecommedations
},
true);
}

return removeFromUnwantedPromise.then(() =>
this.jsonEditingService.write(extensionsFileResource,
{
key: 'recommendations',
value: folderRecommendations
},
true)
);
});
}

protected getFolderRecommendedExtensions(extensionsFileResource: URI): TPromise<string[]> {
return this.fileService.resolveContent(extensionsFileResource)
.then(content => {
const folderRecommendations = (<IExtensionsContent>json.parse(content.value));
return folderRecommendations.recommendations || [];
}, err => []);
}

private getOrUpdateWorkspaceConfigurationFile(workspaceConfigurationFile: URI): TPromise<IContent> {
return this.fileService.resolveContent(workspaceConfigurationFile)
.then(content => {
Expand Down Expand Up @@ -1927,6 +1996,70 @@ export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends Abstrac
}
}

export class AddToWorkspaceRecommendationsAction extends AbstractConfigureRecommendedExtensionsAction {

static readonly ID = 'workbench.extensions.action.addToWorkspaceRecommendations';
static LABEL = localize('addToWorkspaceRecommendations', "Add to workspace recommendations");

constructor(
id: string,
label: string,
@IFileService fileService: IFileService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IEditorService editorService: IEditorService,
@IJSONEditingService jsonEditingService: IJSONEditingService,
@ITextModelService textModelResolverService: ITextModelService,
@ICommandService private commandService: ICommandService,
@INotificationService private notificationService: INotificationService
) {
super(
id,
label,
contextService,
fileService,
editorService,
jsonEditingService,
textModelResolverService
);
}

run(): TPromise<void> {
if (!(this.editorService.activeEditor instanceof ExtensionsInput) || !this.editorService.activeEditor.extension) {
return TPromise.as(null);
}
const folders = this.contextService.getWorkspace().folders;
if (!folders || !folders.length) {
this.notificationService.info(localize('AddToWorkspaceRecommendations.noWorkspace', 'There is no workspace open to add recommendations.'));
return TPromise.as(null);
}

const extensionId = this.editorService.activeEditor.extension.id;
const pickFolderPromise = folders.length === 1
? TPromise.as(folders[0])
: this.commandService.executeCommand<IWorkspaceFolder>(PICK_WORKSPACE_FOLDER_COMMAND_ID);
return pickFolderPromise
.then(workspaceFolder => {
if (!workspaceFolder) {
return TPromise.as(null);
}
const configurationFile = workspaceFolder.toResource(paths.join('.vscode', 'extensions.json'));
return this.getFolderRecommendedExtensions(configurationFile).then(recommendations => {
const extensionIdLowerCase = extensionId.toLowerCase();
if (recommendations.some(e => e.toLowerCase() === extensionIdLowerCase)) {
this.notificationService.info(localize('AddToWorkspaceRecommendations.alreadyExists', 'This extension is already present in workspace recommendations.'));
return TPromise.as(null);
}

return this.addRecommendedExtensionToFolder(configurationFile, extensionId).then(() => {
this.notificationService.info(localize('AddToWorkspaceRecommendations.success', 'The extension was successfully added to workspace recommendations.'));
}, err => {
this.notificationService.error(localize('AddToWorkspaceRecommendations.failure', 'Failed to write to extensions.json. {0}', err));
});
});
});
}
}

export class MaliciousStatusLabelAction extends Action {

private static readonly Class = 'malicious-status';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,10 @@ suite('ExtensionsActions Test', () => {
});
});

test(`RecommendToFolderAction`, () => {
// TODO: Implement test
});

function aLocalExtension(name: string = 'someext', manifest: any = {}, properties: any = {}): ILocalExtension {
const localExtension = <ILocalExtension>Object.create({ manifest: {} });
assign(localExtension, { type: LocalExtensionType.User, manifest: {}, location: URI.file(`pub.${name}`) }, properties);
Expand All @@ -1219,4 +1223,4 @@ suite('ExtensionsActions Test', () => {
return { firstPage: objects, total: objects.length, pageSize: objects.length, getPage: () => null };
}

});
});

0 comments on commit 5eea6bc

Please sign in to comment.