@@ -7,7 +7,7 @@ import * as nls from 'vs/nls';
7
7
import { STATUS_BAR_HOST_NAME_BACKGROUND , STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme' ;
8
8
import { themeColorFromId } from 'vs/platform/theme/common/themeService' ;
9
9
import { IRemoteAgentService , remoteConnectionLatencyMeasurer } from 'vs/workbench/services/remote/common/remoteAgentService' ;
10
- import { RunOnceScheduler } from 'vs/base/common/async' ;
10
+ import { RunOnceScheduler , retry } from 'vs/base/common/async' ;
11
11
import { Event } from 'vs/base/common/event' ;
12
12
import { Disposable , dispose } from 'vs/base/common/lifecycle' ;
13
13
import { MenuId , IMenuService , MenuItemAction , MenuRegistry , registerAction2 , Action2 , SubmenuItemAction } from 'vs/platform/actions/common/actions' ;
@@ -18,12 +18,12 @@ import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/c
18
18
import { ICommandService } from 'vs/platform/commands/common/commands' ;
19
19
import { Schemas } from 'vs/base/common/network' ;
20
20
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions' ;
21
- import { QuickPickItem , IQuickInputService } from 'vs/platform/quickinput/common/quickInput' ;
21
+ import { QuickPickItem , IQuickInputService , IQuickInputButton } from 'vs/platform/quickinput/common/quickInput' ;
22
22
import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService' ;
23
23
import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection' ;
24
24
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver' ;
25
25
import { IHostService } from 'vs/workbench/services/host/browser/host' ;
26
- import { PlatformToString , isWeb , platform } from 'vs/base/common/platform' ;
26
+ import { PlatformName , PlatformToString , isWeb , platform } from 'vs/base/common/platform' ;
27
27
import { once } from 'vs/base/common/functional' ;
28
28
import { truncate } from 'vs/base/common/strings' ;
29
29
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace' ;
@@ -32,7 +32,7 @@ import { getVirtualWorkspaceLocation } from 'vs/platform/workspace/common/virtua
32
32
import { getCodiconAriaLabel } from 'vs/base/common/iconLabels' ;
33
33
import { ILogService } from 'vs/platform/log/common/log' ;
34
34
import { ReloadWindowAction } from 'vs/workbench/browser/actions/windowActions' ;
35
- import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement' ;
35
+ import { EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT , IExtensionGalleryService , IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement' ;
36
36
import { IExtensionsViewPaneContainer , LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID , VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions' ;
37
37
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation' ;
38
38
import { IMarkdownString , MarkdownString } from 'vs/base/common/htmlContent' ;
@@ -46,6 +46,12 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
46
46
import { IProductService } from 'vs/platform/product/common/productService' ;
47
47
import { DomEmitter } from 'vs/base/browser/event' ;
48
48
import { registerColor } from 'vs/platform/theme/common/colorRegistry' ;
49
+ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions' ;
50
+ import { CancellationToken , CancellationTokenSource } from 'vs/base/common/cancellation' ;
51
+ import { ThemeIcon } from 'vs/base/common/themables' ;
52
+ import { infoIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons' ;
53
+ import { IOpenerService } from 'vs/platform/opener/common/opener' ;
54
+ import { URI } from 'vs/base/common/uri' ;
49
55
50
56
export const STATUS_BAR_OFFLINE_BACKGROUND = registerColor ( 'statusBar.offlineBackground' , {
51
57
dark : '#6c1717' ,
@@ -62,6 +68,19 @@ export const STATUS_BAR_OFFLINE_FOREGROUND = registerColor('statusBar.offlineFor
62
68
} , nls . localize ( 'statusBarOfflineForeground' , "Status bar foreground color when the workbench is offline. The status bar is shown in the bottom of the window" ) ) ;
63
69
64
70
type ActionGroup = [ string , Array < MenuItemAction | SubmenuItemAction > ] ;
71
+
72
+ interface RemoteExtensionMetadata {
73
+ id : string ;
74
+ installed : boolean ;
75
+ dependencies : string [ ] ;
76
+ isPlatformCompatible : boolean ;
77
+ helpLink : string ;
78
+ startConnectLabel : string ;
79
+ startCommand : string ;
80
+ priority : number ;
81
+ supportedPlatforms ?: PlatformName [ ] ;
82
+ }
83
+
65
84
export class RemoteStatusIndicator extends Disposable implements IWorkbenchContribution {
66
85
67
86
private static readonly REMOTE_ACTIONS_COMMAND_ID = 'workbench.action.remote.showMenu' ;
@@ -93,7 +112,8 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
93
112
private measureNetworkConnectionLatencyScheduler : RunOnceScheduler | undefined = undefined ;
94
113
95
114
private loggedInvalidGroupNames : { [ group : string ] : boolean } = Object . create ( null ) ;
96
-
115
+ private readonly remoteExtensionMetadata : RemoteExtensionMetadata [ ] ;
116
+ private _isInitialized : boolean = false ;
97
117
constructor (
98
118
@IStatusbarService private readonly statusbarService : IStatusbarService ,
99
119
@IBrowserWorkbenchEnvironmentService private readonly environmentService : IBrowserWorkbenchEnvironmentService ,
@@ -111,9 +131,29 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
111
131
@IExtensionGalleryService private readonly extensionGalleryService : IExtensionGalleryService ,
112
132
@ITelemetryService private readonly telemetryService : ITelemetryService ,
113
133
@IProductService private readonly productService : IProductService ,
134
+ @IExtensionManagementService private readonly extensionManagementService : IExtensionManagementService ,
135
+ @IOpenerService private readonly openerService : IOpenerService ,
114
136
) {
115
137
super ( ) ;
116
138
139
+ const remoteExtensionTips = { ...this . productService . remoteExtensionTips , ...this . productService . virtualWorkspaceExtensionTips } ;
140
+ this . remoteExtensionMetadata = Object . values ( remoteExtensionTips ) . filter ( value => value . startEntry !== undefined ) . map ( value => {
141
+ return {
142
+ id : value . extensionId ,
143
+ installed : false ,
144
+ friendlyName : value . friendlyName ,
145
+ isPlatformCompatible : false ,
146
+ dependencies : [ ] ,
147
+ helpLink : value . startEntry ?. helpLink ?? '' ,
148
+ startConnectLabel : value . startEntry ?. startConnectLabel ?? '' ,
149
+ startCommand : value . startEntry ?. startCommand ?? '' ,
150
+ priority : value . startEntry ?. priority ?? 10 ,
151
+ supportedPlatforms : value . supportedPlatforms
152
+ } ;
153
+ } ) ;
154
+
155
+ this . remoteExtensionMetadata . sort ( ( ext1 , ext2 ) => ext1 . priority - ext2 . priority ) ;
156
+
117
157
// Set initial connection state
118
158
if ( this . remoteAuthority ) {
119
159
this . connectionState = 'initializing' ;
@@ -127,6 +167,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
127
167
128
168
this . updateWhenInstalledExtensionsRegistered ( ) ;
129
169
this . updateRemoteStatusIndicator ( ) ;
170
+ this . initializeRemoteMetadata ( ) ;
130
171
}
131
172
132
173
private registerActions ( ) : void {
@@ -253,6 +294,53 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
253
294
this . _register ( new DomEmitter ( window , 'offline' ) ) . event
254
295
) ( ( ) => this . setNetworkState ( navigator . onLine ? 'online' : 'offline' ) ) ) ;
255
296
}
297
+
298
+ this . _register ( this . extensionService . onDidChangeExtensions ( async ( result ) => {
299
+ for ( const ext of result . added ) {
300
+ const index = this . remoteExtensionMetadata . findIndex ( value => ExtensionIdentifier . equals ( value . id , ext . identifier ) ) ;
301
+ if ( index > - 1 ) {
302
+ this . remoteExtensionMetadata [ index ] . installed = true ;
303
+ }
304
+ }
305
+ } ) ) ;
306
+
307
+ this . _register ( this . extensionManagementService . onDidUninstallExtension ( async ( result ) => {
308
+ const index = this . remoteExtensionMetadata . findIndex ( value => ExtensionIdentifier . equals ( value . id , result . identifier . id ) ) ;
309
+ if ( index > - 1 ) {
310
+ this . remoteExtensionMetadata [ index ] . installed = false ;
311
+ }
312
+ } ) ) ;
313
+ }
314
+
315
+ private async initializeRemoteMetadata ( ) : Promise < void > {
316
+
317
+ if ( this . _isInitialized ) {
318
+ return ;
319
+ }
320
+
321
+ const currentPlatform = PlatformToString ( platform ) ;
322
+ for ( let i = 0 ; i < this . remoteExtensionMetadata . length ; i ++ ) {
323
+ const extensionId = this . remoteExtensionMetadata [ i ] . id ;
324
+ const supportedPlatforms = this . remoteExtensionMetadata [ i ] . supportedPlatforms ;
325
+ // Update compatibility
326
+ const token = new CancellationTokenSource ( ) ;
327
+ const galleryExtension = ( await this . extensionGalleryService . getExtensions ( [ { id : extensionId } ] , token . token ) ) [ 0 ] ;
328
+ if ( ! await this . extensionManagementService . canInstall ( galleryExtension ) ) {
329
+ this . remoteExtensionMetadata [ i ] . isPlatformCompatible = false ;
330
+ }
331
+ else if ( supportedPlatforms && ! supportedPlatforms . includes ( currentPlatform ) ) {
332
+ this . remoteExtensionMetadata [ i ] . isPlatformCompatible = false ;
333
+ }
334
+ else {
335
+ this . remoteExtensionMetadata [ i ] . isPlatformCompatible = true ;
336
+ this . remoteExtensionMetadata [ i ] . dependencies = galleryExtension . properties . extensionPack ?? [ ] ;
337
+ }
338
+
339
+ // Check if installed and enabled
340
+ this . remoteExtensionMetadata [ i ] . installed = ( await this . extensionManagementService . getInstalled ( ) ) . find ( value => ExtensionIdentifier . equals ( value . identifier . id , extensionId ) ) ? true : false ;
341
+ }
342
+
343
+ this . _isInitialized = true ;
256
344
}
257
345
258
346
private updateVirtualWorkspaceLocation ( ) {
@@ -547,6 +635,32 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
547
635
return markdownTooltip ;
548
636
}
549
637
638
+ private async installAndRunStartCommand ( metadata : RemoteExtensionMetadata ) {
639
+ const extensionId = metadata . id ;
640
+ const galleryExtension = ( await this . extensionGalleryService . getExtensions ( [ { id : extensionId } ] , CancellationToken . None ) ) [ 0 ] ;
641
+
642
+ await this . extensionManagementService . installFromGallery ( galleryExtension , {
643
+ isMachineScoped : false ,
644
+ donotIncludePackAndDependencies : false ,
645
+ context : { [ EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT ] : true }
646
+ } ) ;
647
+
648
+ await retry ( async ( ) => {
649
+ const ext = await this . extensionService . getExtension ( metadata . id ) ;
650
+ if ( ! ext ) {
651
+ throw Error ( 'Failed to find installed remote extension' ) ;
652
+ }
653
+ return ext ;
654
+ } , 300 , 10 ) ;
655
+ this . commandService . executeCommand ( metadata . startCommand ) ;
656
+
657
+ this . telemetryService . publicLog2 < WorkbenchActionExecutedEvent , WorkbenchActionExecutedClassification > ( 'workbenchActionExecuted' , {
658
+ id : 'remoteInstallAndRun' ,
659
+ detail : extensionId ,
660
+ from : 'remote indicator'
661
+ } ) ;
662
+ }
663
+
550
664
private showRemoteMenu ( ) {
551
665
const getCategoryLabel = ( action : MenuItemAction ) => {
552
666
if ( action . item . category ) {
@@ -642,19 +756,31 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
642
756
}
643
757
}
644
758
645
- if ( this . extensionGalleryService . isEnabled ( ) && this . hasAdditionalRemoteExtensions ( ) ) {
646
- items . push ( {
647
- id : RemoteStatusIndicator . INSTALL_REMOTE_EXTENSIONS_ID ,
648
- label : nls . localize ( 'installRemotes' , "Install Additional Remote Extensions..." ) ,
649
-
650
- alwaysShow : true
651
- } ) ;
652
- }
653
-
654
759
if ( items . length === entriesBeforeConfig ) {
655
760
items . pop ( ) ; // remove the separator again
656
761
}
657
762
763
+ if ( this . extensionGalleryService . isEnabled ( ) ) {
764
+
765
+ const notInstalledItems : QuickPickItem [ ] = [ ] ;
766
+ for ( const metadata of this . remoteExtensionMetadata ) {
767
+ if ( ! metadata . installed && metadata . isPlatformCompatible ) {
768
+ // Create Install QuickPick with a help link
769
+ const label = metadata . startConnectLabel ;
770
+ const buttons : IQuickInputButton [ ] = [ {
771
+ iconClass : ThemeIcon . asClassName ( infoIcon ) ,
772
+ tooltip : nls . localize ( 'remote.startActions.help' , "Learn More" )
773
+ } ] ;
774
+ notInstalledItems . push ( { type : 'item' , id : metadata . id , label : label , buttons : buttons } ) ;
775
+ }
776
+ }
777
+
778
+ items . push ( {
779
+ type : 'separator' , label : nls . localize ( 'remote.startActions.install' , 'Install' )
780
+ } ) ;
781
+ items . push ( ...notInstalledItems ) ;
782
+ }
783
+
658
784
return items ;
659
785
} ;
660
786
@@ -663,20 +789,36 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
663
789
quickPick . items = computeItems ( ) ;
664
790
quickPick . sortByLabel = false ;
665
791
quickPick . canSelectMany = false ;
666
- once ( quickPick . onDidAccept ) ( ( _ => {
792
+ once ( quickPick . onDidAccept ) ( ( async _ => {
667
793
const selectedItems = quickPick . selectedItems ;
668
794
if ( selectedItems . length === 1 ) {
669
795
const commandId = selectedItems [ 0 ] . id ! ;
670
- this . telemetryService . publicLog2 < WorkbenchActionExecutedEvent , WorkbenchActionExecutedClassification > ( 'workbenchActionExecuted' , {
671
- id : commandId ,
672
- from : 'remote indicator'
673
- } ) ;
674
- this . commandService . executeCommand ( commandId ) ;
796
+ const remoteExtension = this . remoteExtensionMetadata . find ( value => ExtensionIdentifier . equals ( value . id , commandId ) ) ;
797
+ if ( remoteExtension ) {
798
+ quickPick . items = [ ] ;
799
+ quickPick . busy = true ;
800
+ quickPick . placeholder = nls . localize ( 'remote.startActions.installingExtension' , 'Installing extension... ' ) ;
801
+ quickPick . hide ( ) ;
802
+ await this . installAndRunStartCommand ( remoteExtension ) ;
803
+ }
804
+ else {
805
+ this . telemetryService . publicLog2 < WorkbenchActionExecutedEvent , WorkbenchActionExecutedClassification > ( 'workbenchActionExecuted' , {
806
+ id : commandId ,
807
+ from : 'remote indicator'
808
+ } ) ;
809
+ this . commandService . executeCommand ( commandId ) ;
810
+ }
811
+ quickPick . hide ( ) ;
675
812
}
676
-
677
- quickPick . hide ( ) ;
678
813
} ) ) ;
679
814
815
+ once ( quickPick . onDidTriggerItemButton ) ( async ( e ) => {
816
+ const remoteExtension = this . remoteExtensionMetadata . find ( value => ExtensionIdentifier . equals ( value . id , e . item . id ) ) ;
817
+ if ( remoteExtension ) {
818
+ await this . openerService . open ( URI . parse ( remoteExtension . helpLink ) ) ;
819
+ }
820
+ } ) ;
821
+
680
822
// refresh the items when actions change
681
823
const legacyItemUpdater = this . legacyIndicatorMenu . onDidChange ( ( ) => quickPick . items = computeItems ( ) ) ;
682
824
quickPick . onDidHide ( legacyItemUpdater . dispose ) ;
@@ -687,21 +829,6 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
687
829
quickPick . show ( ) ;
688
830
}
689
831
690
- private hasAdditionalRemoteExtensions ( ) {
691
- const extensionTips = { ...this . productService . remoteExtensionTips , ...this . productService . virtualWorkspaceExtensionTips } ;
692
- const currentPlatform = PlatformToString ( platform ) ;
693
- for ( const extension of Object . values ( extensionTips ) ) {
694
- const { extensionId : recommendedExtensionId , supportedPlatforms } = extension ;
695
- if ( ! supportedPlatforms || supportedPlatforms . includes ( currentPlatform ) ) {
696
- // if this recommended extension isn't already installed, return early
697
- if ( ! this . extensionService . extensions . some ( ( extension ) => extension . id ?. toLowerCase ( ) === recommendedExtensionId . toLowerCase ( ) ) ) {
698
- return true ;
699
- }
700
- }
701
- }
702
- return false ;
703
- }
704
-
705
832
private hasRemoteMenuCommands ( ignoreInstallAdditional : boolean ) : boolean {
706
833
if ( this . remoteAuthority !== undefined || this . virtualWorkspaceLocation !== undefined ) {
707
834
if ( RemoteStatusIndicator . SHOW_CLOSE_REMOTE_COMMAND_ID ) {
0 commit comments