44//
55
66using System . Collections . Concurrent ;
7+ using System . Collections . Generic ;
78using System . Linq ;
89using System . Management . Automation ;
910using System . Threading . Tasks ;
@@ -16,21 +17,43 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext
1617 /// </summary>
1718 internal static class CommandHelpers
1819 {
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" ,
2131
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 > ( ) ;
3457
3558 /// <summary>
3659 /// Gets the CommandInfo instance for a command with a particular name.
@@ -45,12 +68,19 @@ public static async Task<CommandInfo> GetCommandInfoAsync(
4568 Validate . IsNotNull ( nameof ( commandName ) , commandName ) ;
4669 Validate . IsNotNull ( nameof ( powerShellContext ) , powerShellContext ) ;
4770
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
5180 // a major slowdown in IntelliSense.
5281 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 ) )
5484 {
5585 return null ;
5686 }
@@ -60,10 +90,18 @@ public static async Task<CommandInfo> GetCommandInfoAsync(
6090 command . AddArgument ( commandName ) ;
6191 command . AddParameter ( "ErrorAction" , "Ignore" ) ;
6292
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 ) )
6494 . Select ( o => o . BaseObject )
6595 . OfType < CommandInfo > ( )
6696 . 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 ;
67105 }
68106
69107 /// <summary>
@@ -87,6 +125,15 @@ public static async Task<string> GetCommandSynopsisAsync(
87125 return string . Empty ;
88126 }
89127
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+
90137 PSCommand command = new PSCommand ( )
91138 . AddCommand ( @"Microsoft.PowerShell.Core\Get-Help" )
92139 // We use .Name here instead of just passing in commandInfo because
@@ -102,6 +149,12 @@ public static async Task<string> GetCommandSynopsisAsync(
102149 ( string ) helpObject ? . Properties [ "synopsis" ] . Value ??
103150 string . Empty ;
104151
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+
105158 // Ignore the placeholder value for this field
106159 if ( string . Equals ( synopsisString , "SHORT DESCRIPTION" , System . StringComparison . CurrentCultureIgnoreCase ) )
107160 {
0 commit comments