Skip to content

Commit 9cd797a

Browse files
Enhance get_command for tools and the Command Explorer
The handler previously serialized the entire command table (names, modules, and full parameter metadata) on every request, which is slow enough to hang the `get_command` language model tool and made the Command Explorer take minutes to populate. Extend `GetCommandParams` so callers can ask for only what they need: - `Name`/`Module` (both wildcard-capable) scope the `Get-Command` call so we don't materialize everything; an unmatched filter writes a non-terminating error, so we pass `-ErrorAction Ignore` and return an empty list instead. - `ExcludeParameters` takes a fast path that returns just name, module, and the new `ModuleVersion` without touching `Parameters`/`ParameterSets`, whose resolution and serialization dominate the cost. - Editor-injected commands are always skipped: the PSES host's fake `PSConsoleHostReadLine` (version 0.0.0) and VS Code's shell-integration helpers (`__VSCode-Escape-Value`, `Set-MappedKeyHandler[s]`) are plumbing, not commands a user authored or imported. - `ExcludeDefaultFunctions` (opt-in) drops PowerShell's module-less default-session functions (`cd..`, `prompt`, `TabExpansion2`, ...) and the install's `pwsh.profile.resource` script. The names come from `InitialSessionState.CreateDefault2()` so the list stays correct across PowerShell versions; module-provided commands are never affected. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3c45bc0 commit 9cd797a

1 file changed

Lines changed: 157 additions & 4 deletions

File tree

src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommandHandler.cs

Lines changed: 157 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Licensed under the MIT License.
33

44
using System.Collections.Generic;
5+
using System.Linq;
56
using System.Management.Automation;
7+
using System.Management.Automation.Runspaces;
68
using System.Threading;
79
using System.Threading.Tasks;
810
using MediatR;
@@ -14,7 +16,37 @@ namespace Microsoft.PowerShell.EditorServices.Handlers
1416
[Serial, Method("powerShell/getCommand", Direction.ClientToServer)]
1517
internal interface IGetCommandHandler : IJsonRpcRequestHandler<GetCommandParams, List<PSCommandMessage>> { }
1618

17-
internal class GetCommandParams : IRequest<List<PSCommandMessage>> { }
19+
internal class GetCommandParams : IRequest<List<PSCommandMessage>>
20+
{
21+
/// <summary>
22+
/// An optional name (supports wildcards) to scope the returned commands.
23+
/// When omitted, all commands are returned.
24+
/// </summary>
25+
public string Name { get; set; }
26+
27+
/// <summary>
28+
/// An optional module name (supports wildcards) to scope the returned
29+
/// commands. When omitted, commands from all modules are returned.
30+
/// </summary>
31+
public string Module { get; set; }
32+
33+
/// <summary>
34+
/// When true, the expensive parameter and parameter-set metadata is not
35+
/// resolved or returned. Callers that only need command names and modules
36+
/// (such as the Command Explorer tree) should set this to avoid the large
37+
/// serialization cost of the full command table.
38+
/// </summary>
39+
public bool ExcludeParameters { get; set; }
40+
41+
/// <summary>
42+
/// When true, module-less functions and scripts that PowerShell's default
43+
/// session provides (e.g. cd.., prompt, TabExpansion2) are omitted. These
44+
/// are interactive shell conveniences and engine plumbing rather than
45+
/// commands a user authored or imported, so the Command Explorer hides them.
46+
/// Module-provided commands (including built-in modules) are never affected.
47+
/// </summary>
48+
public bool ExcludeDefaultFunctions { get; set; }
49+
}
1850

1951
/// <summary>
2052
/// Describes the message to get the details for a single PowerShell Command
@@ -24,6 +56,7 @@ internal class PSCommandMessage
2456
{
2557
public string Name { get; set; }
2658
public string ModuleName { get; set; }
59+
public string ModuleVersion { get; set; }
2760
public string DefaultParameterSet { get; set; }
2861
public Dictionary<string, ParameterMetadata> Parameters { get; set; }
2962
public System.Collections.ObjectModel.ReadOnlyCollection<CommandParameterSetInfo> ParameterSets { get; set; }
@@ -39,11 +72,28 @@ public async Task<List<PSCommandMessage>> Handle(GetCommandParams request, Cance
3972
{
4073
PSCommand psCommand = new();
4174

42-
// Executes the following:
43-
// Get-Command -CommandType Function,Cmdlet,ExternalScript | Sort-Object -Property Name
75+
// Executes the following, scoping by name and/or module when provided
76+
// so we don't serialize the entire command table (which is expensive):
77+
// Get-Command -CommandType Function,Cmdlet,ExternalScript [-Name <name>] [-Module <module>] | Sort-Object -Property Name
4478
psCommand
4579
.AddCommand(@"Microsoft.PowerShell.Core\Get-Command")
46-
.AddParameter("CommandType", new[] { "Function", "Cmdlet", "ExternalScript" })
80+
.AddParameter("CommandType", new[] { "Function", "Cmdlet", "ExternalScript" });
81+
82+
if (!string.IsNullOrEmpty(request.Name))
83+
{
84+
psCommand.AddParameter("Name", request.Name);
85+
}
86+
87+
if (!string.IsNullOrEmpty(request.Module))
88+
{
89+
psCommand.AddParameter("Module", request.Module);
90+
}
91+
92+
// A name or module filter that matches nothing writes a non-terminating
93+
// error; ignore it so we simply return an empty list instead.
94+
psCommand.AddParameter("ErrorAction", "Ignore");
95+
96+
psCommand
4797
.AddCommand(@"Microsoft.PowerShell.Utility\Sort-Object")
4898
.AddParameter("Property", "Name");
4999

@@ -54,6 +104,39 @@ public async Task<List<PSCommandMessage>> Handle(GetCommandParams request, Cance
54104
{
55105
foreach (CommandInfo command in result)
56106
{
107+
// Skip commands injected by the editor's terminal integration
108+
// (the PSES host's fake PSConsoleHostReadLine and VS Code's
109+
// shell-integration helpers); they are implementation details,
110+
// not real commands the user authored or imported.
111+
if (IsEditorInjectedCommand(command))
112+
{
113+
continue;
114+
}
115+
116+
// Optionally drop PowerShell's default-session shell functions
117+
// (and the install's profile-resource script), which are
118+
// module-less and not meaningful in the command list.
119+
if (request.ExcludeDefaultFunctions
120+
&& IsDefaultSessionFunction(command))
121+
{
122+
continue;
123+
}
124+
125+
// When only names/modules are requested, skip resolving the
126+
// parameter metadata entirely. Accessing Parameters/ParameterSets
127+
// forces PowerShell to compute (and we then serialize) the full
128+
// metadata, which is the dominant cost for the whole command table.
129+
if (request.ExcludeParameters)
130+
{
131+
commandList.Add(new PSCommandMessage
132+
{
133+
Name = command.Name,
134+
ModuleName = command.ModuleName,
135+
ModuleVersion = command.Version?.ToString()
136+
});
137+
continue;
138+
}
139+
57140
// Some info objects have a quicker way to get the DefaultParameterSet. These
58141
// are also the most likely to show up so win-win.
59142
string defaultParameterSet = null;
@@ -84,6 +167,7 @@ public async Task<List<PSCommandMessage>> Handle(GetCommandParams request, Cance
84167
{
85168
Name = command.Name,
86169
ModuleName = command.ModuleName,
170+
ModuleVersion = command.Version?.ToString(),
87171
Parameters = command.Parameters,
88172
ParameterSets = command.ParameterSets,
89173
DefaultParameterSet = defaultParameterSet
@@ -93,5 +177,74 @@ public async Task<List<PSCommandMessage>> Handle(GetCommandParams request, Cance
93177

94178
return commandList;
95179
}
180+
181+
// Names of helper functions injected by VS Code's terminal shell
182+
// integration script (shellIntegration.ps1), which the PSES host executes.
183+
// These are editor plumbing rather than user- or module-provided commands.
184+
private static readonly HashSet<string> s_shellIntegrationFunctions = new(System.StringComparer.OrdinalIgnoreCase)
185+
{
186+
"__VSCode-Escape-Value",
187+
"Set-MappedKeyHandler",
188+
"Set-MappedKeyHandlers"
189+
};
190+
191+
// Identifies commands injected by the editor's terminal integration that
192+
// should not be surfaced as real commands.
193+
private static bool IsEditorInjectedCommand(CommandInfo command)
194+
{
195+
if (command.CommandType != CommandTypes.Function)
196+
{
197+
return false;
198+
}
199+
200+
// The fake global PSConsoleHostReadLine function that the PSES host
201+
// defines for terminal shell integration (see PsesInternalHost.cs) has
202+
// no real version, whereas the genuine PSReadLine export always reports
203+
// a real version, so that export is never matched here.
204+
if (command.Name == "PSConsoleHostReadLine"
205+
&& (command.Version is null
206+
|| (command.Version.Major == 0
207+
&& command.Version.Minor == 0
208+
&& command.Version.Build <= 0
209+
&& command.Version.Revision <= 0)))
210+
{
211+
return true;
212+
}
213+
214+
return s_shellIntegrationFunctions.Contains(command.Name);
215+
}
216+
217+
// The names of the functions that PowerShell's default session state
218+
// provides (cd.., cd\, cd~, Clear-Host, exec, help, oss, Pause, prompt,
219+
// TabExpansion2). Enumerated once from InitialSessionState so the list stays
220+
// correct across PowerShell versions rather than being hard-coded.
221+
private static readonly System.Lazy<HashSet<string>> s_defaultSessionFunctions = new(() =>
222+
new HashSet<string>(
223+
InitialSessionState.CreateDefault2().Commands
224+
.OfType<SessionStateFunctionEntry>()
225+
.Select(static entry => entry.Name),
226+
System.StringComparer.OrdinalIgnoreCase));
227+
228+
// Identifies module-less functions and scripts that PowerShell's default
229+
// session provides — interactive shell conveniences and engine plumbing that
230+
// aren't meaningful in the command list. Only matches commands with no module,
231+
// so a module-provided command (including built-in modules) is never affected.
232+
private static bool IsDefaultSessionFunction(CommandInfo command)
233+
{
234+
if (!string.IsNullOrEmpty(command.ModuleName))
235+
{
236+
return false;
237+
}
238+
239+
// The profile-resource script shipped alongside the PowerShell install
240+
// (e.g. pwsh.profile.resource.ps1) is install plumbing, not a user script.
241+
if (command.CommandType == CommandTypes.ExternalScript)
242+
{
243+
return command.Name.StartsWith("pwsh.profile.resource", System.StringComparison.OrdinalIgnoreCase);
244+
}
245+
246+
return command.CommandType == CommandTypes.Function
247+
&& s_defaultSessionFunctions.Value.Contains(command.Name);
248+
}
96249
}
97250
}

0 commit comments

Comments
 (0)