diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 026353790fdec742bc92224e75b29ed0c00fc95d..f605d18f232879ce16c96ad9c6a0661f2397773c 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -342,7 +342,7 @@ export interface IExtensionTipsService { _serviceBrand: any; getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason, reasonText: string }; }; getFileBasedRecommendations(): string[]; - getOtherRecommendations(): string[]; + getOtherRecommendations(): TPromise; getWorkspaceRecommendations(): TPromise; getKeymapRecommendations(): string[]; getKeywordsForExtension(extension: string): string[]; diff --git a/src/vs/workbench/parts/extensions/common/extensions.ts b/src/vs/workbench/parts/extensions/common/extensions.ts index d26c46d117b8efd1c71d461bc9b379a76a917469..9582535c2b276e8f8bbab4a7f76a06775ce4318b 100644 --- a/src/vs/workbench/parts/extensions/common/extensions.ts +++ b/src/vs/workbench/parts/extensions/common/extensions.ts @@ -86,8 +86,10 @@ export interface IExtensionsWorkbenchService { export const ConfigurationKey = 'extensions'; export const AutoUpdateConfigurationKey = 'extensions.autoUpdate'; +export const DisableEagerRecommendationsKey = 'extensions.disableEagerRecommendations'; export interface IExtensionsConfiguration { autoUpdate: boolean; ignoreRecommendations: boolean; + disableEagerRecommendations: boolean; } diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts index 5be419ee0692424ad4dd3effc25eb78839dbfcb0..4ebb44bba1be01826a30e74be8402ea0c59e5158 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionTipsService.ts @@ -22,7 +22,7 @@ import Severity from 'vs/base/common/severity'; import { IWorkspaceContextService, IWorkspaceFolder, IWorkspace, IWorkspaceFoldersChangeEvent, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { Schemas } from 'vs/base/common/network'; import { IFileService } from 'vs/platform/files/common/files'; -import { IExtensionsConfiguration, ConfigurationKey } from 'vs/workbench/parts/extensions/common/extensions'; +import { IExtensionsConfiguration, ConfigurationKey, DisableEagerRecommendationsKey } from 'vs/workbench/parts/extensions/common/extensions'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import * as pfs from 'vs/base/node/pfs'; @@ -63,6 +63,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe private _extensionsRecommendationsUrl: string; private _disposables: IDisposable[] = []; public promptWorkspaceRecommendationsPromise: TPromise; + private proactiveRecommendationsFetched: boolean = false; constructor( @IExtensionGalleryService private _galleryService: IExtensionGalleryService, @@ -89,15 +90,39 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe if (product.extensionsGallery && product.extensionsGallery.recommendationsUrl) { this._extensionsRecommendationsUrl = product.extensionsGallery.recommendationsUrl; } - this.getDynamicWorkspaceRecommendations(); - this._suggestFileBasedRecommendations(); + this.getCachedDynamicWorkspaceRecommendations(); + this._suggestFileBasedRecommendations(); this.promptWorkspaceRecommendationsPromise = this._suggestWorkspaceRecommendations(); - // Executable based recommendations carry out a lot of file stats, so run them after 10 secs - // So that the startup is not affected - setTimeout(() => this._suggestBasedOnExecutables(this._exeBasedRecommendations), 10000); + if (!this.configurationService.getValue(DisableEagerRecommendationsKey)) { + this.fetchProactiveRecommendations(true); + } + this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e))); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (!this.proactiveRecommendationsFetched && !this.configurationService.getValue(DisableEagerRecommendationsKey)) { + this.fetchProactiveRecommendations(); + } + })); + } + + private fetchProactiveRecommendations(calledDuringStartup?: boolean): TPromise { + let fetchPromise = TPromise.as(null); + if (!this.proactiveRecommendationsFetched) { + this.proactiveRecommendationsFetched = true; + + // Executable based recommendations carry out a lot of file stats, so run them after 10 secs + // So that the startup is not affected + + fetchPromise = new TPromise((c, e) => { + setTimeout(() => { + TPromise.join([this._suggestBasedOnExecutables(), this.getDynamicWorkspaceRecommendations()]).then(() => c(null)); + }, calledDuringStartup ? 10000 : 0); + }); + + } + return fetchPromise; } private isEnabled(): boolean { @@ -107,6 +132,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason, reasonText: string }; } { let output: { [id: string]: { reasonId: ExtensionRecommendationReason, reasonText: string }; } = Object.create(null); + if (!this.proactiveRecommendationsFetched) { + return output; + } + if (this.contextService.getWorkspace().folders && this.contextService.getWorkspace().folders.length === 1) { const currentRepo = this.contextService.getWorkspace().folders[0].name; this._dynamicWorkspaceRecommendations.forEach(x => output[x.toLowerCase()] = { @@ -238,10 +267,12 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return fileBased; } - getOtherRecommendations(): string[] { - const others = distinct([...Object.keys(this._exeBasedRecommendations), ...this._dynamicWorkspaceRecommendations]); - shuffle(others); - return others; + getOtherRecommendations(): TPromise { + return this.fetchProactiveRecommendations().then(() => { + const others = distinct([...Object.keys(this._exeBasedRecommendations), ...this._dynamicWorkspaceRecommendations]); + shuffle(others); + return others; + }); } getKeymapRecommendations(): string[] { @@ -342,7 +373,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe ); const config = this.configurationService.getValue(ConfigurationKey); - if (config.ignoreRecommendations) { + if (config.ignoreRecommendations || config.disableEagerRecommendations) { return; } @@ -521,7 +552,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe const config = this.configurationService.getValue(ConfigurationKey); return this.getWorkspaceRecommendations().then(allRecommendations => { - if (!allRecommendations.length || config.ignoreRecommendations || this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false)) { + if (!allRecommendations.length || config.ignoreRecommendations || config.disableEagerRecommendations || this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false)) { return; } @@ -609,7 +640,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe }); } - private _suggestBasedOnExecutables(recommendations: { [id: string]: string; }): void { + private _suggestBasedOnExecutables(): TPromise { const homeDir = os.homedir(); let foundExecutables: Set = new Set(); @@ -620,13 +651,14 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe (product.exeBasedExtensionTips[exeName]['recommendations'] || []) .forEach(x => { if (product.exeBasedExtensionTips[exeName]['friendlyName']) { - recommendations[x] = product.exeBasedExtensionTips[exeName]['friendlyName']; + this._exeBasedRecommendations[x] = product.exeBasedExtensionTips[exeName]['friendlyName']; } }); } }); }; + let promises: TPromise[] = []; // Loop through recommended extensions forEach(product.exeBasedExtensionTips, entry => { if (typeof entry.value !== 'object' || !Array.isArray(entry.value['recommendations'])) { @@ -643,12 +675,14 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe .replace('%ProgramFiles(x86)%', process.env['ProgramFiles(x86)']) .replace('%ProgramFiles%', process.env['ProgramFiles']) .replace('%APPDATA%', process.env['APPDATA']); - findExecutable(exeName, windowsPath); + promises.push(findExecutable(exeName, windowsPath)); } else { - findExecutable(exeName, paths.join('/usr/local/bin', exeName)); - findExecutable(exeName, paths.join(homeDir, exeName)); + promises.push(findExecutable(exeName, paths.join('/usr/local/bin', exeName))); + promises.push(findExecutable(exeName, paths.join(homeDir, exeName))); } }); + + return TPromise.join(promises); } private setIgnoreRecommendationsConfig(configVal: boolean) { @@ -659,9 +693,9 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } } - private getDynamicWorkspaceRecommendations(): TPromise { + private getCachedDynamicWorkspaceRecommendations() { if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER) { - return TPromise.as(null); + return; } const storageKey = 'extensionsAssistant/dynamicWorkspaceRecommendations'; @@ -684,13 +718,17 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe } */ this.telemetryService.publicLog('dynamicWorkspaceRecommendations', { count: this._dynamicWorkspaceRecommendations.length, cache: 1 }); - return TPromise.as(null); } + } - if (!this._extensionsRecommendationsUrl) { + private getDynamicWorkspaceRecommendations(): TPromise { + if (this.contextService.getWorkbenchState() !== WorkbenchState.FOLDER + || this._dynamicWorkspaceRecommendations.length + || !this._extensionsRecommendationsUrl) { return TPromise.as(null); } + const storageKey = 'extensionsAssistant/dynamicWorkspaceRecommendations'; const workspaceUri = this.contextService.getWorkspace().folders[0].uri; return TPromise.join([getHashedRemotesFromUri(workspaceUri, this.fileService, false), getHashedRemotesFromUri(workspaceUri, this.fileService, true)]).then(([hashedRemotes1, hashedRemotes2]) => { const hashedRemotes = (hashedRemotes1 || []).concat(hashedRemotes2 || []); @@ -698,43 +736,37 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe return null; } - return new TPromise((c, e) => { - setTimeout(() => { - this.requestService.request({ type: 'GET', url: this._extensionsRecommendationsUrl }).then(context => { - if (context.res.statusCode !== 200) { - return c(null); - } - return asJson(context).then((result) => { - const allRecommendations: IDynamicWorkspaceRecommendations[] = Array.isArray(result['workspaceRecommendations']) ? result['workspaceRecommendations'] : []; - if (!allRecommendations.length) { - return c(null); - } + return this.requestService.request({ type: 'GET', url: this._extensionsRecommendationsUrl }).then(context => { + if (context.res.statusCode !== 200) { + return TPromise.as(null); + } + return asJson(context).then((result) => { + const allRecommendations: IDynamicWorkspaceRecommendations[] = Array.isArray(result['workspaceRecommendations']) ? result['workspaceRecommendations'] : []; + if (!allRecommendations.length) { + return; + } - let foundRemote = false; - for (let i = 0; i < hashedRemotes.length && !foundRemote; i++) { - for (let j = 0; j < allRecommendations.length && !foundRemote; j++) { - if (Array.isArray(allRecommendations[j].remoteSet) && allRecommendations[j].remoteSet.indexOf(hashedRemotes[i]) > -1) { - foundRemote = true; - this._dynamicWorkspaceRecommendations = allRecommendations[j].recommendations || []; - this.storageService.store(storageKey, JSON.stringify({ - recommendations: this._dynamicWorkspaceRecommendations, - timestamp: Date.now() - }), StorageScope.WORKSPACE); - /* __GDPR__ - "dynamicWorkspaceRecommendations" : { - "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "cache" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('dynamicWorkspaceRecommendations', { count: this._dynamicWorkspaceRecommendations.length, cache: 0 }); + let foundRemote = false; + for (let i = 0; i < hashedRemotes.length && !foundRemote; i++) { + for (let j = 0; j < allRecommendations.length && !foundRemote; j++) { + if (Array.isArray(allRecommendations[j].remoteSet) && allRecommendations[j].remoteSet.indexOf(hashedRemotes[i]) > -1) { + foundRemote = true; + this._dynamicWorkspaceRecommendations = allRecommendations[j].recommendations || []; + this.storageService.store(storageKey, JSON.stringify({ + recommendations: this._dynamicWorkspaceRecommendations, + timestamp: Date.now() + }), StorageScope.WORKSPACE); + /* __GDPR__ + "dynamicWorkspaceRecommendations" : { + "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "cache" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } } - } + */ + this.telemetryService.publicLog('dynamicWorkspaceRecommendations', { count: this._dynamicWorkspaceRecommendations.length, cache: 0 }); } - - return c(null); - }); - }); - }, 10000); + } + } + }); }); }); } 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 178a39a6886284077204d071b359116948391426..e2c57a1e462bdbb65db2fa2357e6e6a8773cb044 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensions.contribution.ts @@ -208,6 +208,11 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'boolean', description: localize('extensionsIgnoreRecommendations', "If set to true, the notifications for extension recommendations will stop showing up."), default: false + }, + 'extensions.disableEagerRecommendations': { + type: 'boolean', + description: localize('extensionsDisableEagerRecommendations', "If set to true, no recommendation is fetched or shown unless specifically requested by the user."), + default: false } } }); diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts index 7c51274b7ff675d89f309ef17c0852fcaafa65f1..f723b458e69904bbd56a2ce90eb5648a1c526fa8 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViewlet.ts @@ -25,7 +25,7 @@ import { append, $, addStandardDisposableListener, EventType, addClass, removeCl import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/platform/extensions/common/extensions'; -import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, ExtensionState, AutoUpdateConfigurationKey } from '../common/extensions'; +import { IExtensionsWorkbenchService, IExtensionsViewlet, VIEWLET_ID, ExtensionState, AutoUpdateConfigurationKey, DisableEagerRecommendationsKey } from '../common/extensions'; import { ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowDisabledExtensionsAction, ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, @@ -64,6 +64,7 @@ const NonEmptyWorkspaceContext = new RawContextKey('nonEmptyWorkspace', const SearchExtensionsContext = new RawContextKey('searchExtensions', false); const SearchInstalledExtensionsContext = new RawContextKey('searchInstalledExtensions', false); const RecommendedExtensionsContext = new RawContextKey('recommendedExtensions', false); +const DefaultRecommendedExtensionsContext = new RawContextKey('defaultRecommendedExtensions', false); export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtensionsViewlet { @@ -72,6 +73,7 @@ export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtens private searchExtensionsContextKey: IContextKey; private searchInstalledExtensionsContextKey: IContextKey; private recommendedExtensionsContextKey: IContextKey; + private defaultRecommendedExtensionsContextKey: IContextKey; private searchDelayer: ThrottledDelayer; private root: HTMLElement; @@ -107,7 +109,8 @@ export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtens this.searchExtensionsContextKey = SearchExtensionsContext.bindTo(contextKeyService); this.searchInstalledExtensionsContextKey = SearchInstalledExtensionsContext.bindTo(contextKeyService); this.recommendedExtensionsContextKey = RecommendedExtensionsContext.bindTo(contextKeyService); - + this.defaultRecommendedExtensionsContextKey = DefaultRecommendedExtensionsContext.bindTo(contextKeyService); + this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(DisableEagerRecommendationsKey)); this.disposables.push(this.viewletService.onDidViewletOpen(this.onViewletOpen, this, this.disposables)); this.configurationService.onDidChangeConfiguration(e => { @@ -115,6 +118,9 @@ export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtens this.secondaryActions = null; this.updateTitleArea(); } + if (e.affectedKeys.indexOf(DisableEagerRecommendationsKey) > -1) { + this.defaultRecommendedExtensionsContextKey.set(!this.configurationService.getValue(DisableEagerRecommendationsKey)); + } }, this, this.disposables); } @@ -168,7 +174,7 @@ export class ExtensionsViewlet extends PersistentViewsViewlet implements IExtens name: localize('recommendedExtensions', "Recommended"), location: ViewLocation.Extensions, ctor: RecommendedExtensionsView, - when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions')), + when: ContextKeyExpr.and(ContextKeyExpr.not('searchExtensions'), ContextKeyExpr.has('defaultRecommendedExtensions')), weight: 70, canToggleVisibility: true }; diff --git a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts index eef0647b527370004f2392368c1eb361cdfdca26..ef3fd2d9c5556afe9278626a2549b04d285064d5 100644 --- a/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts +++ b/src/vs/workbench/parts/extensions/electron-browser/extensionsViews.ts @@ -278,10 +278,11 @@ export class ExtensionsListView extends ViewsViewletPanel { .then(local => { const installedExtensions = local.map(x => `${x.publisher}.${x.name}`); let fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); - let others = this.tipsService.getOtherRecommendations(); + const othersPromise = this.tipsService.getOtherRecommendations(); + const workspacePromise = this.tipsService.getWorkspaceRecommendations(); - return this.tipsService.getWorkspaceRecommendations() - .then(workspaceRecommendations => { + return TPromise.join([othersPromise, workspacePromise]) + .then(([others, workspaceRecommendations]) => { const names = this.getTrimmedRecommendations(installedExtensions, value, fileBasedRecommendations, others, workspaceRecommendations); /* __GDPR__ @@ -311,10 +312,11 @@ export class ExtensionsListView extends ViewsViewletPanel { .then(local => { const installedExtensions = local.map(x => `${x.publisher}.${x.name}`); let fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); - let others = this.tipsService.getOtherRecommendations(); + const othersPromise = this.tipsService.getOtherRecommendations(); + const workspacePromise = this.tipsService.getWorkspaceRecommendations(); - return this.tipsService.getWorkspaceRecommendations() - .then(workspaceRecommendations => { + return TPromise.join([othersPromise, workspacePromise]) + .then(([others, workspaceRecommendations]) => { workspaceRecommendations = workspaceRecommendations.map(x => x.toLowerCase()); fileBasedRecommendations = fileBasedRecommendations.filter(x => workspaceRecommendations.indexOf(x.toLowerCase()) === -1); others = others.filter(x => workspaceRecommendations.indexOf(x.toLowerCase()) === -1); diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts index 32969237d69014b54c2a3c1a23498c70b5101336..e997b15603a6c1bae2a10a70533054b6d7d26850 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts @@ -227,7 +227,7 @@ suite('ExtensionsTipsService Test', () => { } }); - testConfigurationService.setUserConfiguration(ConfigurationKey, { ignoreRecommendations: false }); + testConfigurationService.setUserConfiguration(ConfigurationKey, { ignoreRecommendations: false, disableEagerRecommendations: false }); instantiationService.stub(IStorageService, { get: (a, b, c) => c, getBoolean: (a, b, c) => c, store: () => { } }); instantiationService.stub(IModelService, { getModels(): any { return []; }, @@ -333,6 +333,17 @@ suite('ExtensionsTipsService Test', () => { return testNoPromptForValidRecommendations(mockTestData.validRecommendedExtensions); }); + test('ExtensionTipsService: No Prompt for valid workspace recommendations if disableEagerRecommendations is set', () => { + testConfigurationService.setUserConfiguration(ConfigurationKey, { disableEagerRecommendations: true }); + return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { + testObject = instantiationService.createInstance(ExtensionTipsService); + return testObject.promptWorkspaceRecommendationsPromise.then(() => { + assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, 0); + assert.ok(!prompted); + }); + }); + }); + test('ExtensionTipsService: No Prompt for valid workspace recommendations if ignoreRecommendations is set for current workspace', () => { instantiationService.stub(IStorageService, { get: (a, b, c) => c, getBoolean: (a, b, c) => a === 'extensionsAssistant/workspaceRecommendationsIgnore' || c }); return testNoPromptForValidRecommendations(mockTestData.validRecommendedExtensions);