Skip to content

Commit 0e9b982

Browse files
Signaturehelp cancellation and some caching (#1251)
* SignatureHelp cancellation & some caching in CommandHelpers * add comment * better language * fixed wording * address feedback from Keith and Patrick * move to hashset, remove static ctr
1 parent 8c500ee commit 0e9b982

File tree

3 files changed

+96
-44
lines changed

3 files changed

+96
-44
lines changed

src/PowerShellEditorServices/Services/PowerShellContext/Utilities/CommandHelpers.cs

+72-19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//
55

66
using System.Collections.Concurrent;
7+
using System.Collections.Generic;
78
using System.Linq;
89
using System.Management.Automation;
910
using 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
{

src/PowerShellEditorServices/Services/TextDocument/Handlers/HoverHandler.cs

+1-4
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,17 @@ internal class HoverHandler : IHoverHandler
2222
private readonly ILogger _logger;
2323
private readonly SymbolsService _symbolsService;
2424
private readonly WorkspaceService _workspaceService;
25-
private readonly PowerShellContextService _powerShellContextService;
2625

2726
private HoverCapability _capability;
2827

2928
public HoverHandler(
3029
ILoggerFactory factory,
3130
SymbolsService symbolsService,
32-
WorkspaceService workspaceService,
33-
PowerShellContextService powerShellContextService)
31+
WorkspaceService workspaceService)
3432
{
3533
_logger = factory.CreateLogger<HoverHandler>();
3634
_symbolsService = symbolsService;
3735
_workspaceService = workspaceService;
38-
_powerShellContextService = powerShellContextService;
3936
}
4037

4138
public HoverRegistrationOptions GetRegistrationOptions()

src/PowerShellEditorServices/Services/TextDocument/Handlers/SignatureHelpHandler.cs

+23-21
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
//
55

6-
using System;
7-
using System.Linq;
6+
using System.Collections.Generic;
87
using System.Threading;
98
using System.Threading.Tasks;
109
using Microsoft.Extensions.Logging;
@@ -20,7 +19,6 @@ namespace Microsoft.PowerShell.EditorServices.Handlers
2019
{
2120
internal class SignatureHelpHandler : ISignatureHelpHandler
2221
{
23-
private static readonly SignatureInformation[] s_emptySignatureResult = Array.Empty<SignatureInformation>();
2422
private readonly ILogger _logger;
2523
private readonly SymbolsService _symbolsService;
2624
private readonly WorkspaceService _workspaceService;
@@ -52,6 +50,12 @@ public SignatureHelpRegistrationOptions GetRegistrationOptions()
5250

5351
public async Task<SignatureHelp> Handle(SignatureHelpParams request, CancellationToken cancellationToken)
5452
{
53+
if (cancellationToken.IsCancellationRequested)
54+
{
55+
_logger.LogDebug("SignatureHelp request canceled for file: {0}", request.TextDocument.Uri);
56+
return new SignatureHelp();
57+
}
58+
5559
ScriptFile scriptFile = _workspaceService.GetFile(request.TextDocument.Uri);
5660

5761
ParameterSetSignatures parameterSets =
@@ -61,28 +65,26 @@ await _symbolsService.FindParameterSetsInFileAsync(
6165
(int) request.Position.Character + 1,
6266
_powerShellContextService).ConfigureAwait(false);
6367

64-
SignatureInformation[] signatures = s_emptySignatureResult;
68+
if (parameterSets == null)
69+
{
70+
return new SignatureHelp();
71+
}
6572

66-
if (parameterSets != null)
73+
var signatures = new SignatureInformation[parameterSets.Signatures.Length];
74+
for (int i = 0; i < signatures.Length; i++)
6775
{
68-
signatures = new SignatureInformation[parameterSets.Signatures.Length];
69-
for (int i = 0; i < signatures.Length; i++)
76+
var parameters = new List<ParameterInformation>();
77+
foreach (ParameterInfo param in parameterSets.Signatures[i].Parameters)
7078
{
71-
var parameters = new ParameterInformation[parameterSets.Signatures[i].Parameters.Count()];
72-
int j = 0;
73-
foreach (ParameterInfo param in parameterSets.Signatures[i].Parameters)
74-
{
75-
parameters[j] = CreateParameterInfo(param);
76-
j++;
77-
}
78-
79-
signatures[i] = new SignatureInformation
80-
{
81-
Label = parameterSets.CommandName + " " + parameterSets.Signatures[i].SignatureText,
82-
Documentation = null,
83-
Parameters = parameters,
84-
};
79+
parameters.Add(CreateParameterInfo(param));
8580
}
81+
82+
signatures[i] = new SignatureInformation
83+
{
84+
Label = parameterSets.CommandName + " " + parameterSets.Signatures[i].SignatureText,
85+
Documentation = null,
86+
Parameters = parameters,
87+
};
8688
}
8789

8890
return new SignatureHelp

0 commit comments

Comments
 (0)