44 *--------------------------------------------------------------------------------------------*/
55
66import { localize } from 'vs/nls' ;
7- import { join } from 'vs/base/common/path' ;
7+ import { join , basename } from 'vs/base/common/path' ;
88import { forEach } from 'vs/base/common/collections' ;
99import { Disposable } from 'vs/base/common/lifecycle' ;
1010import { match } from 'vs/base/common/glob' ;
@@ -43,6 +43,8 @@ import { CancellationToken } from 'vs/base/common/cancellation';
4343import { ExtensionType } from 'vs/platform/extensions/common/extensions' ;
4444import { extname } from 'vs/base/common/resources' ;
4545import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles' ;
46+ import { IExeBasedExtensionTip } from 'vs/platform/product/common/product' ;
47+ import { timeout } from 'vs/base/common/async' ;
4648
4749const milliSecondsInADay = 1000 * 60 * 60 * 24 ;
4850const choiceNever = localize ( 'neverShowAgain' , "Don't Show Again" ) ;
@@ -71,7 +73,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
7173 _serviceBrand : any ;
7274
7375 private _fileBasedRecommendations : { [ id : string ] : { recommendedTime : number , sources : ExtensionRecommendationSource [ ] } ; } = Object . create ( null ) ;
74- private _exeBasedRecommendations : { [ id : string ] : string ; } = Object . create ( null ) ;
76+ private _exeBasedRecommendations : { [ id : string ] : IExeBasedExtensionTip ; } = Object . create ( null ) ;
77+ private _importantExeBasedRecommendations : { [ id : string ] : IExeBasedExtensionTip ; } = Object . create ( null ) ;
7578 private _availableRecommendations : { [ pattern : string ] : string [ ] } = Object . create ( null ) ;
7679 private _allWorkspaceRecommendedExtensions : IExtensionRecommendation [ ] = [ ] ;
7780 private _dynamicWorkspaceRecommendations : string [ ] = [ ] ;
@@ -187,7 +190,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
187190
188191 forEach ( this . _exeBasedRecommendations , entry => output [ entry . key . toLowerCase ( ) ] = {
189192 reasonId : ExtensionRecommendationReason . Executable ,
190- reasonText : localize ( 'exeBasedRecommendation' , "This extension is recommended because you have {0} installed." , entry . value )
193+ reasonText : localize ( 'exeBasedRecommendation' , "This extension is recommended because you have {0} installed." , entry . value . friendlyName )
191194 } ) ;
192195
193196 forEach ( this . _fileBasedRecommendations , entry => output [ entry . key . toLowerCase ( ) ] = {
@@ -496,6 +499,100 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
496499
497500 //#endregion
498501
502+ //#region important exe based extension
503+
504+ private async promptForImportantExeBasedExtension ( ) : Promise < boolean > {
505+
506+ const storageKey = 'extensionsAssistant/workspaceRecommendationsIgnore' ;
507+ const config = this . configurationService . getValue < IExtensionsConfiguration > ( ConfigurationKey ) ;
508+
509+ if ( config . ignoreRecommendations
510+ || config . showRecommendationsOnlyOnDemand
511+ || this . storageService . getBoolean ( storageKey , StorageScope . WORKSPACE , false ) ) {
512+ return false ;
513+ }
514+
515+ const installed = await this . extensionManagementService . getInstalled ( ExtensionType . User ) ;
516+ let recommendationsToSuggest = Object . keys ( this . _importantExeBasedRecommendations ) ;
517+ recommendationsToSuggest = this . filterAllIgnoredInstalledAndNotAllowed ( recommendationsToSuggest , installed ) ;
518+ if ( recommendationsToSuggest . length === 0 ) {
519+ return false ;
520+ }
521+
522+ const id = recommendationsToSuggest [ 0 ] ;
523+ const tip = this . _importantExeBasedRecommendations [ id ] ;
524+ const message = localize ( 'exeRecommended' , "The '{0}' extension is recommended as you have {1} installed on your system." , tip . friendlyName ! , tip . exeFriendlyName || basename ( tip . windowsPath ! ) ) ;
525+
526+ this . notificationService . prompt ( Severity . Info , message ,
527+ [ {
528+ label : localize ( 'install' , 'Install' ) ,
529+ run : ( ) => {
530+ /* __GDPR__
531+ "exeExtensionRecommendations:popup" : {
532+ "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
533+ "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
534+ }
535+ */
536+ this . telemetryService . publicLog ( 'exeExtensionRecommendations:popup' , { userReaction : 'install' , extensionId : name } ) ;
537+ this . instantiationService . createInstance ( InstallRecommendedExtensionAction , id ) . run ( ) ;
538+ }
539+ } , {
540+ label : localize ( 'showRecommendations' , "Show Recommendations" ) ,
541+ run : ( ) => {
542+ /* __GDPR__
543+ "exeExtensionRecommendations:popup" : {
544+ "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
545+ "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
546+ }
547+ */
548+ this . telemetryService . publicLog ( 'exeExtensionRecommendations:popup' , { userReaction : 'show' , extensionId : name } ) ;
549+
550+ const recommendationsAction = this . instantiationService . createInstance ( ShowRecommendedExtensionsAction , ShowRecommendedExtensionsAction . ID , localize ( 'showRecommendations' , "Show Recommendations" ) ) ;
551+ recommendationsAction . run ( ) ;
552+ recommendationsAction . dispose ( ) ;
553+ }
554+ } , {
555+ label : choiceNever ,
556+ isSecondary : true ,
557+ run : ( ) => {
558+ this . addToImportantRecommendationsIgnore ( id ) ;
559+ /* __GDPR__
560+ "exeExtensionRecommendations:popup" : {
561+ "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
562+ "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
563+ }
564+ */
565+ this . telemetryService . publicLog ( 'exeExtensionRecommendations:popup' , { userReaction : 'neverShowAgain' , extensionId : name } ) ;
566+ this . notificationService . prompt (
567+ Severity . Info ,
568+ localize ( 'ignoreExtensionRecommendations' , "Do you want to ignore all extension recommendations?" ) ,
569+ [ {
570+ label : localize ( 'ignoreAll' , "Yes, Ignore All" ) ,
571+ run : ( ) => this . setIgnoreRecommendationsConfig ( true )
572+ } , {
573+ label : localize ( 'no' , "No" ) ,
574+ run : ( ) => this . setIgnoreRecommendationsConfig ( false )
575+ } ]
576+ ) ;
577+ }
578+ } ] ,
579+ {
580+ sticky : true ,
581+ onCancel : ( ) => {
582+ /* __GDPR__
583+ "exeExtensionRecommendations:popup" : {
584+ "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
585+ "extensionId": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight" }
586+ }
587+ */
588+ this . telemetryService . publicLog ( 'exeExtensionRecommendations:popup' , { userReaction : 'cancelled' , extensionId : name } ) ;
589+ }
590+ }
591+ ) ;
592+
593+ return true ;
594+ }
595+
499596 //#region fileBasedRecommendations
500597
501598 getFileBasedRecommendations ( ) : IExtensionRecommendation [ ] {
@@ -646,21 +743,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
646743 }
647744
648745 private async promptRecommendedExtensionForFileType ( recommendationsToSuggest : string [ ] , installed : ILocalExtension [ ] ) : Promise < boolean > {
649- const importantRecommendationsIgnoreList = < string [ ] > JSON . parse ( this . storageService . get ( 'extensionsAssistant/importantRecommendationsIgnore' , StorageScope . GLOBAL , '[]' ) ) ;
650- const installedExtensionsIds = installed . reduce ( ( result , i ) => { result . add ( i . identifier . id . toLowerCase ( ) ) ; return result ; } , new Set < string > ( ) ) ;
651- recommendationsToSuggest = recommendationsToSuggest . filter ( id => {
652- if ( importantRecommendationsIgnoreList . indexOf ( id ) !== - 1 ) {
653- return false ;
654- }
655- if ( ! this . isExtensionAllowedToBeRecommended ( id ) ) {
656- return false ;
657- }
658- if ( installedExtensionsIds . has ( id . toLowerCase ( ) ) ) {
659- return false ;
660- }
661- return true ;
662- } ) ;
663746
747+ recommendationsToSuggest = this . filterAllIgnoredInstalledAndNotAllowed ( recommendationsToSuggest , installed ) ;
664748 if ( recommendationsToSuggest . length === 0 ) {
665749 return false ;
666750 }
@@ -674,18 +758,8 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
674758 let message = localize ( 'reallyRecommended2' , "The '{0}' extension is recommended for this file type." , name ) ;
675759 if ( entry . isExtensionPack ) {
676760 message = localize ( 'reallyRecommendedExtensionPack' , "The '{0}' extension pack is recommended for this file type." , name ) ;
677- } else if ( entry . isLocationPattern ) {
678- message = localize ( 'reallyRecommendedLocationPattern' , "The '{0}' extension is recommended for files at this location." , name ) ;
679761 }
680762
681- const setIgnoreRecommendationsConfig = ( configVal : boolean ) => {
682- this . configurationService . updateValue ( 'extensions.ignoreRecommendations' , configVal , ConfigurationTarget . USER ) ;
683- if ( configVal ) {
684- const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore' ;
685- this . storageService . store ( ignoreWorkspaceRecommendationsStorageKey , true , StorageScope . WORKSPACE ) ;
686- }
687- } ;
688-
689763 this . notificationService . prompt ( Severity . Info , message ,
690764 [ {
691765 label : localize ( 'install' , 'Install' ) ,
@@ -718,12 +792,7 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
718792 label : choiceNever ,
719793 isSecondary : true ,
720794 run : ( ) => {
721- importantRecommendationsIgnoreList . push ( id ) ;
722- this . storageService . store (
723- 'extensionsAssistant/importantRecommendationsIgnore' ,
724- JSON . stringify ( importantRecommendationsIgnoreList ) ,
725- StorageScope . GLOBAL
726- ) ;
795+ this . addToImportantRecommendationsIgnore ( id ) ;
727796 /* __GDPR__
728797 "extensionRecommendations:popup" : {
729798 "userReaction" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
@@ -736,10 +805,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
736805 localize ( 'ignoreExtensionRecommendations' , "Do you want to ignore all extension recommendations?" ) ,
737806 [ {
738807 label : localize ( 'ignoreAll' , "Yes, Ignore All" ) ,
739- run : ( ) => setIgnoreRecommendationsConfig ( true )
808+ run : ( ) => this . setIgnoreRecommendationsConfig ( true )
740809 } , {
741810 label : localize ( 'no' , "No" ) ,
742- run : ( ) => setIgnoreRecommendationsConfig ( false )
811+ run : ( ) => this . setIgnoreRecommendationsConfig ( false )
743812 } ]
744813 ) ;
745814 }
@@ -831,6 +900,42 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
831900 ) ;
832901 }
833902
903+ private filterAllIgnoredInstalledAndNotAllowed ( recommendationsToSuggest : string [ ] , installed : ILocalExtension [ ] ) : string [ ] {
904+
905+ const importantRecommendationsIgnoreList = < string [ ] > JSON . parse ( this . storageService . get ( 'extensionsAssistant/importantRecommendationsIgnore' , StorageScope . GLOBAL , '[]' ) ) ;
906+ const installedExtensionsIds = installed . reduce ( ( result , i ) => { result . add ( i . identifier . id . toLowerCase ( ) ) ; return result ; } , new Set < string > ( ) ) ;
907+ return recommendationsToSuggest . filter ( id => {
908+ if ( importantRecommendationsIgnoreList . indexOf ( id ) !== - 1 ) {
909+ return false ;
910+ }
911+ if ( ! this . isExtensionAllowedToBeRecommended ( id ) ) {
912+ return false ;
913+ }
914+ if ( installedExtensionsIds . has ( id . toLowerCase ( ) ) ) {
915+ return false ;
916+ }
917+ return true ;
918+ } ) ;
919+ }
920+
921+ private addToImportantRecommendationsIgnore ( id : string ) {
922+ const importantRecommendationsIgnoreList = < string [ ] > JSON . parse ( this . storageService . get ( 'extensionsAssistant/importantRecommendationsIgnore' , StorageScope . GLOBAL , '[]' ) ) ;
923+ importantRecommendationsIgnoreList . push ( id ) ;
924+ this . storageService . store (
925+ 'extensionsAssistant/importantRecommendationsIgnore' ,
926+ JSON . stringify ( importantRecommendationsIgnoreList ) ,
927+ StorageScope . GLOBAL
928+ ) ;
929+ }
930+
931+ private setIgnoreRecommendationsConfig ( configVal : boolean ) {
932+ this . configurationService . updateValue ( 'extensions.ignoreRecommendations' , configVal , ConfigurationTarget . USER ) ;
933+ if ( configVal ) {
934+ const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore' ;
935+ this . storageService . store ( ignoreWorkspaceRecommendationsStorageKey , true , StorageScope . WORKSPACE ) ;
936+ }
937+ }
938+
834939 //#endregion
835940
836941 //#region otherRecommendations
@@ -857,18 +962,18 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
857962 }
858963
859964 private fetchProactiveRecommendations ( calledDuringStartup ?: boolean ) : Promise < void > {
860- let fetchPromise = Promise . resolve ( undefined ) ;
965+ let fetchPromise = Promise . resolve < any > ( undefined ) ;
861966 if ( ! this . proactiveRecommendationsFetched ) {
862967 this . proactiveRecommendationsFetched = true ;
863968
864- // Executable based recommendations carry out a lot of file stats, so run them after 10 secs
865- // So that the startup is not affected
969+ // Executable based recommendations carry out a lot of file stats, delay the resolution so that the startup is not affected
970+ // 10 sec for regular extensions
971+ // 3 secs for important
866972
867- fetchPromise = new Promise ( ( c , e ) => {
868- setTimeout ( ( ) => {
869- Promise . all ( [ this . fetchExecutableRecommendations ( ) , this . fetchDynamicWorkspaceRecommendations ( ) ] ) . then ( ( ) => c ( undefined ) ) ;
870- } , calledDuringStartup ? 10000 : 0 ) ;
871- } ) ;
973+ const importantExeBasedRecommendations = timeout ( calledDuringStartup ? 3000 : 0 ) . then ( _ => this . fetchExecutableRecommendations ( true ) ) ;
974+ importantExeBasedRecommendations . then ( _ => this . promptForImportantExeBasedExtension ( ) ) ;
975+
976+ fetchPromise = timeout ( calledDuringStartup ? 10000 : 0 ) . then ( _ => Promise . all ( [ this . fetchDynamicWorkspaceRecommendations ( ) , this . fetchExecutableRecommendations ( false ) , importantExeBasedRecommendations ] ) ) ;
872977
873978 }
874979 return fetchPromise ;
@@ -877,20 +982,22 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
877982 /**
878983 * If user has any of the tools listed in product.exeBasedExtensionTips, fetch corresponding recommendations
879984 */
880- private fetchExecutableRecommendations ( ) : Promise < void > {
985+ private fetchExecutableRecommendations ( important : boolean ) : Promise < void > {
881986 const homeDir = os . homedir ( ) ;
882987 let foundExecutables : Set < string > = new Set < string > ( ) ;
883988
884- let findExecutable = ( exeName : string , path : string ) => {
989+ let findExecutable = ( exeName : string , tip : IExeBasedExtensionTip , path : string ) => {
885990 return pfs . fileExists ( path ) . then ( exists => {
886991 if ( exists && ! foundExecutables . has ( exeName ) ) {
887992 foundExecutables . add ( exeName ) ;
888- ( product . exeBasedExtensionTips [ exeName ] [ 'recommendations' ] || [ ] )
889- . forEach ( extensionId => {
890- if ( product . exeBasedExtensionTips [ exeName ] [ 'friendlyName' ] ) {
891- this . _exeBasedRecommendations [ extensionId . toLowerCase ( ) ] = product . exeBasedExtensionTips [ exeName ] [ 'friendlyName' ] ;
993+ ( tip [ 'recommendations' ] || [ ] ) . forEach ( extensionId => {
994+ if ( tip . friendlyName ) {
995+ if ( important ) {
996+ this . _importantExeBasedRecommendations [ extensionId . toLowerCase ( ) ] = tip ;
892997 }
893- } ) ;
998+ this . _exeBasedRecommendations [ extensionId . toLowerCase ( ) ] = tip ;
999+ }
1000+ } ) ;
8941001 }
8951002 } ) ;
8961003 } ;
@@ -901,8 +1008,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
9011008 if ( typeof entry . value !== 'object' || ! Array . isArray ( entry . value [ 'recommendations' ] ) ) {
9021009 return ;
9031010 }
904-
905- let exeName = entry . key ;
1011+ if ( important !== ! ! entry . value . important ) {
1012+ return ;
1013+ }
1014+ const exeName = entry . key ;
9061015 if ( process . platform === 'win32' ) {
9071016 let windowsPath = entry . value [ 'windowsPath' ] ;
9081017 if ( ! windowsPath || typeof windowsPath !== 'string' ) {
@@ -913,10 +1022,10 @@ export class ExtensionTipsService extends Disposable implements IExtensionTipsSe
9131022 . replace ( '%ProgramFiles%' , process . env [ 'ProgramFiles' ] ! )
9141023 . replace ( '%APPDATA%' , process . env [ 'APPDATA' ] ! )
9151024 . replace ( '%WINDIR%' , process . env [ 'WINDIR' ] ! ) ;
916- promises . push ( findExecutable ( exeName , windowsPath ) ) ;
1025+ promises . push ( findExecutable ( exeName , entry . value , windowsPath ) ) ;
9171026 } else {
918- promises . push ( findExecutable ( exeName , join ( '/usr/local/bin' , exeName ) ) ) ;
919- promises . push ( findExecutable ( exeName , join ( homeDir , exeName ) ) ) ;
1027+ promises . push ( findExecutable ( exeName , entry . value , join ( '/usr/local/bin' , exeName ) ) ) ;
1028+ promises . push ( findExecutable ( exeName , entry . value , join ( homeDir , exeName ) ) ) ;
9201029 }
9211030 } ) ;
9221031
0 commit comments