4
4
//
5
5
6
6
using System . Collections . Concurrent ;
7
+ using System . Collections . Generic ;
7
8
using System . Linq ;
8
9
using System . Management . Automation ;
9
10
using System . Threading . Tasks ;
@@ -16,21 +17,43 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext
16
17
/// </summary>
17
18
internal static class CommandHelpers
18
19
{
19
- private static readonly ConcurrentDictionary < string , bool > NounExclusionList =
20
- new ConcurrentDictionary < string , bool > ( ) ;
20
+ private static readonly HashSet < string > s_nounExclusionList = new HashSet < string >
21
+ {
22
+ // PowerShellGet v2 nouns
23
+ "CredsFromCredentialProvider" ,
24
+ "DscResource" ,
25
+ "InstalledModule" ,
26
+ "InstalledScript" ,
27
+ "PSRepository" ,
28
+ "RoleCapability" ,
29
+ "Script" ,
30
+ "ScriptFileInfo" ,
21
31
22
- static CommandHelpers ( )
23
- {
24
- NounExclusionList . TryAdd ( "Module" , true ) ;
25
- NounExclusionList . TryAdd ( "Script" , true ) ;
26
- NounExclusionList . TryAdd ( "Package" , true ) ;
27
- NounExclusionList . TryAdd ( "PackageProvider" , true ) ;
28
- NounExclusionList . TryAdd ( "PackageSource" , true ) ;
29
- NounExclusionList . TryAdd ( "InstalledModule" , true ) ;
30
- NounExclusionList . TryAdd ( "InstalledScript" , true ) ;
31
- NounExclusionList . TryAdd ( "ScriptFileInfo" , true ) ;
32
- NounExclusionList . TryAdd ( "PSRepository" , true ) ;
33
- }
32
+ // PackageManagement nouns
33
+ "Package" ,
34
+ "PackageProvider" ,
35
+ "PackageSource" ,
36
+ } ;
37
+
38
+ // This is used when a noun exists in multiple modules (for example, "Command" is used in Microsoft.PowerShell.Core and also PowerShellGet)
39
+ private static readonly HashSet < string > s_cmdletExclusionList = new HashSet < string >
40
+ {
41
+ // Commands in PowerShellGet with conflicting nouns
42
+ "Find-Command" ,
43
+ "Find-Module" ,
44
+ "Install-Module" ,
45
+ "Publish-Module" ,
46
+ "Save-Module" ,
47
+ "Uninstall-Module" ,
48
+ "Update-Module" ,
49
+ "Update-ModuleManifest" ,
50
+ } ;
51
+
52
+ private static readonly ConcurrentDictionary < string , CommandInfo > s_commandInfoCache =
53
+ new ConcurrentDictionary < string , CommandInfo > ( ) ;
54
+
55
+ private static readonly ConcurrentDictionary < string , string > s_synopsisCache =
56
+ new ConcurrentDictionary < string , string > ( ) ;
34
57
35
58
/// <summary>
36
59
/// Gets the CommandInfo instance for a command with a particular name.
@@ -45,12 +68,19 @@ public static async Task<CommandInfo> GetCommandInfoAsync(
45
68
Validate . IsNotNull ( nameof ( commandName ) , commandName ) ;
46
69
Validate . IsNotNull ( nameof ( powerShellContext ) , powerShellContext ) ;
47
70
48
- // Make sure the command's noun isn't blacklisted. This is
49
- // currently necessary to make sure that Get-Command doesn't
50
- // load PackageManagement or PowerShellGet because they cause
71
+ // If we have a CommandInfo cached, return that.
72
+ if ( s_commandInfoCache . TryGetValue ( commandName , out CommandInfo cmdInfo ) )
73
+ {
74
+ return cmdInfo ;
75
+ }
76
+
77
+ // Make sure the command's noun or command's name isn't in the exclusion lists.
78
+ // This is currently necessary to make sure that Get-Command doesn't
79
+ // load PackageManagement or PowerShellGet v2 because they cause
51
80
// a major slowdown in IntelliSense.
52
81
var commandParts = commandName . Split ( '-' ) ;
53
- if ( commandParts . Length == 2 && NounExclusionList . ContainsKey ( commandParts [ 1 ] ) )
82
+ if ( ( commandParts . Length == 2 && s_nounExclusionList . Contains ( commandParts [ 1 ] ) )
83
+ || s_cmdletExclusionList . Contains ( commandName ) )
54
84
{
55
85
return null ;
56
86
}
@@ -60,10 +90,18 @@ public static async Task<CommandInfo> GetCommandInfoAsync(
60
90
command . AddArgument ( commandName ) ;
61
91
command . AddParameter ( "ErrorAction" , "Ignore" ) ;
62
92
63
- return ( await powerShellContext . ExecuteCommandAsync < PSObject > ( command , sendOutputToHost : false , sendErrorToHost : false ) . ConfigureAwait ( false ) )
93
+ CommandInfo commandInfo = ( await powerShellContext . ExecuteCommandAsync < PSObject > ( command , sendOutputToHost : false , sendErrorToHost : false ) . ConfigureAwait ( false ) )
64
94
. Select ( o => o . BaseObject )
65
95
. OfType < CommandInfo > ( )
66
96
. FirstOrDefault ( ) ;
97
+
98
+ // Only cache CmdletInfos since they're exposed in binaries they are likely to not change throughout the session.
99
+ if ( commandInfo . CommandType == CommandTypes . Cmdlet )
100
+ {
101
+ s_commandInfoCache . TryAdd ( commandName , commandInfo ) ;
102
+ }
103
+
104
+ return commandInfo ;
67
105
}
68
106
69
107
/// <summary>
@@ -87,6 +125,15 @@ public static async Task<string> GetCommandSynopsisAsync(
87
125
return string . Empty ;
88
126
}
89
127
128
+ // If we have a synopsis cached, return that.
129
+ // NOTE: If the user runs Update-Help, it's possible that this synopsis will be out of date.
130
+ // Given the perf increase of doing this, and the simple workaround of restarting the extension,
131
+ // this seems worth it.
132
+ if ( s_synopsisCache . TryGetValue ( commandInfo . Name , out string synopsis ) )
133
+ {
134
+ return synopsis ;
135
+ }
136
+
90
137
PSCommand command = new PSCommand ( )
91
138
. AddCommand ( @"Microsoft.PowerShell.Core\Get-Help" )
92
139
// We use .Name here instead of just passing in commandInfo because
@@ -102,6 +149,12 @@ public static async Task<string> GetCommandSynopsisAsync(
102
149
( string ) helpObject ? . Properties [ "synopsis" ] . Value ??
103
150
string . Empty ;
104
151
152
+ // Only cache cmdlet infos because since they're exposed in binaries, the can never change throughout the session.
153
+ if ( commandInfo . CommandType == CommandTypes . Cmdlet )
154
+ {
155
+ s_synopsisCache . TryAdd ( commandInfo . Name , synopsisString ) ;
156
+ }
157
+
105
158
// Ignore the placeholder value for this field
106
159
if ( string . Equals ( synopsisString , "SHORT DESCRIPTION" , System . StringComparison . CurrentCultureIgnoreCase ) )
107
160
{
0 commit comments