From 70f9bb6a95c2669193050d2b84e44e2aa8366ea8 Mon Sep 17 00:00:00 2001 From: Fernando Tolentino Date: Fri, 5 May 2017 19:21:32 -0300 Subject: [PATCH] Better UX for recommended extensions #13456 --- .../configuration-editing/src/extension.ts | 21 +++++- .../common/extensionManagement.ts | 1 + .../extensions/browser/extensionsActions.ts | 67 +++++++++++++++++++ .../parts/extensions/common/extensions.ts | 2 + .../common/extensionsFileTemplate.ts | 2 +- .../electron-browser/extensionTipsService.ts | 21 ++++++ .../extensions.contribution.ts | 6 +- .../electron-browser/extensionsViewlet.ts | 3 +- .../node/extensionsWorkbenchService.ts | 26 +++++++ .../extensionsActions.test.ts | 5 ++ 10 files changed, 148 insertions(+), 6 deletions(-) diff --git a/extensions/configuration-editing/src/extension.ts b/extensions/configuration-editing/src/extension.ts index 30a1da21e5592..d0028affda928 100644 --- a/extensions/configuration-editing/src/extension.ts +++ b/extensions/configuration-editing/src/extension.ts @@ -24,6 +24,9 @@ export function activate(context): void { //settings.json suggestions context.subscriptions.push(registerSettingsCompletions()); + //extensions.json suggestions + context.subscriptions.push(registerExtensionsCompletions()); + // launch.json decorations context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(editor => updateLaunchJsonDecorations(editor), null, context.subscriptions)); context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(event => { @@ -61,11 +64,25 @@ function registerSettingsCompletions(): vscode.Disposable { }); } -function newSimpleCompletionItem(text: string, range: vscode.Range, description?: string): vscode.CompletionItem { +function registerExtensionsCompletions(): vscode.Disposable { + return vscode.languages.registerCompletionItemProvider({ pattern: '**/extensions.json' }, { + provideCompletionItems(document, position, token) { + const location = getLocation(document.getText(), document.offsetAt(position)); + const range = document.getWordRangeAtPosition(position) || new vscode.Range(position, position); + if (location.path[0] === 'recommendations') { + return vscode.extensions.all + .filter(e => e.id.indexOf('vscode') === -1) + .map(e => newSimpleCompletionItem(e.id, range, undefined, '"' + e.id + '"')); + } + } + }); +} + +function newSimpleCompletionItem(text: string, range: vscode.Range, description?: string, insertText?: string): vscode.CompletionItem { const item = new vscode.CompletionItem(text); item.kind = vscode.CompletionItemKind.Value; item.detail = description; - item.insertText = text; + item.insertText = insertText || text; item.range = range; return item; diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 8df367a9615f6..06b1f75ff14a6 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -287,6 +287,7 @@ export interface IExtensionTipsService { _serviceBrand: any; getRecommendations(): string[]; getWorkspaceRecommendations(): TPromise; + addToWorkspaceRecommendations(extensionId: string): TPromise; getKeymapRecommendations(): string[]; getKeywordsForExtension(extension: string): string[]; getRecommendationsForExtension(extension: string): string[]; diff --git a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts index 748b574f24849..7cad514f17b58 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts @@ -358,6 +358,7 @@ export class ManageExtensionAction extends Action { instantiationService.createInstance(EnableGloballyAction, localize('enableAlwaysAction.label', "Enable (Always)")) ], [ + instantiationService.createInstance(AddToWorkspaceRecommendationsAction, localize('addToWorkspaceRecommendationsAction.label', "Add to Workspace Recommendations")), instantiationService.createInstance(DisableForWorkspaceAction, localize('disableForWorkspaceAction.label', "Disable (Workspace)")), instantiationService.createInstance(DisableGloballyAction, localize('disableAlwaysAction.label', "Disable (Always)")) ], @@ -394,6 +395,48 @@ export class ManageExtensionAction extends Action { } } +export class AddToWorkspaceRecommendationsAction extends Action implements IExtensionAction { + + static ID = 'extensions.addToWorkspaceRecommendationsAction'; + static LABEL = localize('addToWorkspaceRecommendationsAction', "Workspace"); + + private disposables: IDisposable[] = []; + + private _extension: IExtension; + get extension(): IExtension { return this._extension; } + set extension(extension: IExtension) { this._extension = extension; this.update(); } + + constructor(label: string, + @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionEnablementService private extensionEnablementService: IExtensionEnablementService, + @IInstantiationService private instantiationService: IInstantiationService + ) { + super(AddToWorkspaceRecommendationsAction.ID, label); + + this.disposables.push(this.extensionsWorkbenchService.onChange(() => this.update())); + this.update(); + } + + private update(): void { + this.enabled = !!this.extension; + } + + run(): TPromise { + const action = this.instantiationService.createInstance( + ConfigureWorkspaceRecommendedExtensionsAction, + ConfigureWorkspaceRecommendedExtensionsAction.ID, + ConfigureWorkspaceRecommendedExtensionsAction.LABEL + ); + return action.run() + .then(() => this.extensionsWorkbenchService.addToWorkspaceRecommendations(this.extension)); + } + + dispose(): void { + super.dispose(); + this.disposables = dispose(this.disposables); + } +} export class EnableForWorkspaceAction extends Action implements IExtensionAction { static ID = 'extensions.enableForWorkspace'; @@ -1020,6 +1063,30 @@ export class ShowWorkspaceRecommendedExtensionsAction extends Action { } } +export class InstallWorkspaceRecommendedExtensionsAction extends Action { + + static ID = 'workbench.extensions.action.installWorkspaceRecommendedExtensions'; + static LABEL = localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"); + + constructor( + id: string, + label: string, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IViewletService private viewletService: IViewletService, + @IExtensionsWorkbenchService private extensionsWorkbenchService: IExtensionsWorkbenchService + ) { + super(id, label, null, contextService.hasWorkspace()); + } + + run(): TPromise { + return this.extensionsWorkbenchService.installAllWorkspaceRecommendations(); + } + + protected isEnabled(): boolean { + return true; + } +} + export class ShowRecommendedKeymapExtensionsAction extends Action { static ID = 'workbench.extensions.action.showRecommendedKeymapExtensions'; diff --git a/src/vs/workbench/parts/extensions/common/extensions.ts b/src/vs/workbench/parts/extensions/common/extensions.ts index f11cd884312ab..ca861fee6f5e6 100644 --- a/src/vs/workbench/parts/extensions/common/extensions.ts +++ b/src/vs/workbench/parts/extensions/common/extensions.ts @@ -77,6 +77,8 @@ export interface IExtensionsWorkbenchService { loadDependencies(extension: IExtension): TPromise; open(extension: IExtension, sideByside?: boolean): TPromise; checkForUpdates(): TPromise; + addToWorkspaceRecommendations(extension: IExtension): TPromise; + installAllWorkspaceRecommendations(): TPromise; } export const ConfigurationKey = 'extensions'; diff --git a/src/vs/workbench/parts/extensions/common/extensionsFileTemplate.ts b/src/vs/workbench/parts/extensions/common/extensionsFileTemplate.ts index d97f7986c3582..695f04b9edd1e 100644 --- a/src/vs/workbench/parts/extensions/common/extensionsFileTemplate.ts +++ b/src/vs/workbench/parts/extensions/common/extensionsFileTemplate.ts @@ -18,7 +18,7 @@ export const ExtensionsConfigurationSchema: IJSONSchema = { description: localize('app.extensions.json.recommendations', "List of extensions recommendations. The identifier of an extension is always '${publisher}.${name}'. For example: 'vscode.csharp'."), items: { type: 'string', - defaultSnippets: [{ label: 'Example', body: 'vscode.csharp' }], + defaultSnippets: [], pattern: EXTENSION_IDENTIFIER_PATTERN, errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") }, diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index 78bf2b91c7135..2e0273539515a 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts @@ -79,6 +79,27 @@ export class ExtensionTipsService implements IExtensionTipsService { }, err => []); } + addToWorkspaceRecommendations(extensionId: string): TPromise { + if (!this.contextService.hasWorkspace()) { + return TPromise.as(void 0); + } + const resource = this.contextService.toResource(paths.join('.vscode', 'extensions.json')); + return this.fileService.resolveContent(resource).then(content => { + const extensionsContent = json.parse(content.value, []); + const regEx = new RegExp(EXTENSION_IDENTIFIER_PATTERN); + let recommendations = extensionsContent.recommendations || []; + recommendations = recommendations.filter((element, position) => { + return recommendations.indexOf(element) === position && regEx.test(element); + }); + if (recommendations.indexOf(extensionId) === -1) { + recommendations.push(extensionId); + } + extensionsContent.recommendations = recommendations; + return this.fileService.updateContent(resource, JSON.stringify(extensionsContent, null, 2)) + .then(() => recommendations); + }, err => []).then(() => void 0); + } + getRecommendations(): string[] { return Object.keys(this._recommendations); } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts index 827ae6984e184..0f29a55579509 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts @@ -22,8 +22,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { VIEWLET_ID, IExtensionsWorkbenchService } from '../common/extensions'; import { ExtensionsWorkbenchService } from 'vs/workbench/parts/extensions/node/extensionsWorkbenchService'; import { - OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowWorkspaceRecommendedExtensionsAction, ShowPopularExtensionsAction, - ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, UpdateAllAction, ConfigureWorkspaceRecommendedExtensionsAction, + OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, UpdateAllAction, ConfigureWorkspaceRecommendedExtensionsAction, EnableAllAction, EnableAllWorkpsaceAction, DisableAllAction, DisableAllWorkpsaceAction, CheckForUpdatesAction } from 'vs/workbench/parts/extensions/browser/extensionsActions'; import { OpenExtensionsFolderAction, InstallVSIXAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; @@ -118,6 +117,9 @@ actionRegistry.registerWorkbenchAction(keymapRecommendationsActionDescriptor, 'P const workspaceRecommendationsActionDescriptor = new SyncActionDescriptor(ShowWorkspaceRecommendedExtensionsAction, ShowWorkspaceRecommendedExtensionsAction.ID, ShowWorkspaceRecommendedExtensionsAction.LABEL); actionRegistry.registerWorkbenchAction(workspaceRecommendationsActionDescriptor, 'Extensions: Show Workspace Recommended Extensions', ExtensionsLabel); +const installWorkspaceRecommendationsActionDescriptor = new SyncActionDescriptor(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, InstallWorkspaceRecommendedExtensionsAction.LABEL); +actionRegistry.registerWorkbenchAction(installWorkspaceRecommendationsActionDescriptor, 'Extensions: Install Workspace Recommended Extensions', ExtensionsLabel); + const popularActionDescriptor = new SyncActionDescriptor(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL); actionRegistry.registerWorkbenchAction(popularActionDescriptor, 'Extensions: Show Popular Extensions', ExtensionsLabel); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts index 73345e9c66be7..fe6e95d569018 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -31,7 +31,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { Delegate, Renderer } from 'vs/workbench/parts/extensions/browser/extensionsList'; import { IExtensionsWorkbenchService, IExtension, IExtensionsViewlet, VIEWLET_ID, ExtensionState } from '../common/extensions'; import { - ShowRecommendedExtensionsAction, ShowWorkspaceRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, + ShowRecommendedExtensionsAction, ShowWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction } from 'vs/workbench/parts/extensions/browser/extensionsActions'; import { InstallVSIXAction } from 'vs/workbench/parts/extensions/electron-browser/extensionsActions'; @@ -199,6 +199,7 @@ export class ExtensionsViewlet extends Viewlet implements IExtensionsViewlet { this.instantiationService.createInstance(ShowDisabledExtensionsAction, ShowDisabledExtensionsAction.ID, ShowDisabledExtensionsAction.LABEL), this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL), this.instantiationService.createInstance(ShowWorkspaceRecommendedExtensionsAction, ShowWorkspaceRecommendedExtensionsAction.ID, ShowWorkspaceRecommendedExtensionsAction.LABEL), + this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction.ID, InstallWorkspaceRecommendedExtensionsAction.LABEL), this.instantiationService.createInstance(ShowRecommendedKeymapExtensionsAction, ShowRecommendedKeymapExtensionsAction.ID, ShowRecommendedKeymapExtensionsAction.LABEL), this.instantiationService.createInstance(ShowPopularExtensionsAction, ShowPopularExtensionsAction.ID, ShowPopularExtensionsAction.LABEL), new Separator(), diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index bcaad136e0458..2b10664eac92a 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -799,6 +799,32 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService { }); } + addToWorkspaceRecommendations(extension: IExtension): TPromise { + return this.tipsService.addToWorkspaceRecommendations(extension.id); + } + + installAllWorkspaceRecommendations(): TPromise { + return this.tipsService.getWorkspaceRecommendations() + .then(extensions => { + this.queryGallery({ names: extensions }) + .done(result => { + if (result.total < 1) { + return; + } + + const extension = result.firstPage[0]; + const promises = [this.open(extension)]; + + if (this.local.every(local => local.id !== extension.id)) { + promises.push(this.install(extension)); + } + + TPromise.join(promises) + .done(null, error => this.onError(error)); + }); + }); + } + dispose(): void { this.syncDelayer.cancel(); this.disposables = dispose(this.disposables); diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsActions.test.ts index ca746ff803c1b..2136b4f2174c8 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsActions.test.ts @@ -489,6 +489,11 @@ suite('ExtensionsActions Test', () => { }); }); + test('Test AddToWorkspaceRecommendationsAction when there is no extension', () => { + const testObject: ExtensionsActions.AddToWorkspaceRecommendationsAction = instantiationService.createInstance(ExtensionsActions.AddToWorkspaceRecommendationsAction, 'id'); + assert.ok(!testObject.enabled); + }); + test('Test EnableForWorkspaceAction when there is no extension', () => { const testObject: ExtensionsActions.EnableForWorkspaceAction = instantiationService.createInstance(ExtensionsActions.EnableForWorkspaceAction, 'id');