Skip to content

Commit eca10bf

Browse files
Add a powerShell/getModule handler for module metadata
The Command Explorer groups commands under versioned module nodes and shows a tooltip on hover. Add a `getModule` request that returns a single module's metadata (version, description, path, author, company, project URI, required PowerShell version) so the client can populate those tooltips lazily, and register the handler in `PsesLanguageServer`. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 76f341e commit eca10bf

3 files changed

Lines changed: 180 additions & 0 deletions

File tree

src/PowerShellEditorServices/Server/PsesLanguageServer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ public async Task StartAsync()
121121
.WithHandler<GetCommentHelpHandler>()
122122
.WithHandler<EvaluateHandler>()
123123
.WithHandler<GetCommandHandler>()
124+
.WithHandler<GetModuleHandler>()
124125
.WithHandler<ShowHelpHandler>()
125126
.WithHandler<ExpandAliasHandler>()
126127
.WithHandler<PsesSemanticTokensHandler>()
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Generic;
5+
using System.Management.Automation;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using MediatR;
9+
using OmniSharp.Extensions.JsonRpc;
10+
using Microsoft.PowerShell.EditorServices.Services.PowerShell;
11+
using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution;
12+
13+
namespace Microsoft.PowerShell.EditorServices.Handlers
14+
{
15+
[Serial, Method("powerShell/getModule", Direction.ClientToServer)]
16+
internal interface IGetModuleHandler : IJsonRpcRequestHandler<GetModuleParams, PSModuleMessage> { }
17+
18+
internal class GetModuleParams : IRequest<PSModuleMessage>
19+
{
20+
/// <summary>
21+
/// The name of the module to retrieve metadata for.
22+
/// </summary>
23+
public string Name { get; set; }
24+
25+
/// <summary>
26+
/// An optional specific version of the module. When omitted, the newest
27+
/// available version is returned.
28+
/// </summary>
29+
public string Version { get; set; }
30+
}
31+
32+
/// <summary>
33+
/// Describes the metadata for a single PowerShell module, used to populate
34+
/// the Command Explorer's module tooltips.
35+
/// </summary>
36+
internal class PSModuleMessage
37+
{
38+
public string Name { get; set; }
39+
public string Version { get; set; }
40+
public string Description { get; set; }
41+
public string Path { get; set; }
42+
public string Author { get; set; }
43+
public string CompanyName { get; set; }
44+
public string ProjectUri { get; set; }
45+
public string PowerShellVersion { get; set; }
46+
}
47+
48+
internal class GetModuleHandler : IGetModuleHandler
49+
{
50+
private readonly IInternalPowerShellExecutionService _executionService;
51+
52+
public GetModuleHandler(IInternalPowerShellExecutionService executionService) => _executionService = executionService;
53+
54+
public async Task<PSModuleMessage> Handle(GetModuleParams request, CancellationToken cancellationToken)
55+
{
56+
if (string.IsNullOrEmpty(request.Name))
57+
{
58+
return null;
59+
}
60+
61+
// Resolve a module's metadata from the available modules, pinning to a
62+
// specific version when requested and otherwise taking the newest.
63+
const string GetModuleScript = @"
64+
[System.Diagnostics.DebuggerHidden()]
65+
[CmdletBinding()]
66+
param (
67+
[String]$Name,
68+
[String]$Version
69+
)
70+
$modules = Microsoft.PowerShell.Core\Get-Module -ListAvailable -Name $Name -ErrorAction Ignore
71+
if ($Version) {
72+
$modules = $modules | Microsoft.PowerShell.Core\Where-Object { $_.Version.ToString() -eq $Version }
73+
}
74+
$module = $modules | Microsoft.PowerShell.Utility\Sort-Object Version -Descending | Microsoft.PowerShell.Utility\Select-Object -First 1
75+
if ($null -eq $module) {
76+
return
77+
}
78+
[PSCustomObject]@{
79+
Name = $module.Name
80+
Version = $module.Version.ToString()
81+
Description = $module.Description
82+
Path = $module.Path
83+
Author = $module.Author
84+
CompanyName = $module.CompanyName
85+
ProjectUri = if ($module.ProjectUri) { $module.ProjectUri.ToString() } else { '' }
86+
PowerShellVersion = if ($module.PowerShellVersion) { $module.PowerShellVersion.ToString() } else { '' }
87+
}
88+
";
89+
90+
PSCommand getModuleCommand = new PSCommand()
91+
.AddScript(GetModuleScript, useLocalScope: true)
92+
.AddParameter("Name", request.Name)
93+
.AddParameter("Version", request.Version);
94+
95+
IReadOnlyList<PSObject> results = await _executionService.ExecutePSCommandAsync<PSObject>(
96+
getModuleCommand,
97+
cancellationToken,
98+
new PowerShellExecutionOptions
99+
{
100+
ThrowOnError = false
101+
}).ConfigureAwait(false);
102+
103+
PSObject result = results is { Count: > 0 } ? results[0] : null;
104+
if (result is null)
105+
{
106+
return null;
107+
}
108+
109+
return new PSModuleMessage
110+
{
111+
Name = GetPropertyString(result, "Name"),
112+
Version = GetPropertyString(result, "Version"),
113+
Description = GetPropertyString(result, "Description"),
114+
Path = GetPropertyString(result, "Path"),
115+
Author = GetPropertyString(result, "Author"),
116+
CompanyName = GetPropertyString(result, "CompanyName"),
117+
ProjectUri = GetPropertyString(result, "ProjectUri"),
118+
PowerShellVersion = GetPropertyString(result, "PowerShellVersion")
119+
};
120+
}
121+
122+
private static string GetPropertyString(PSObject psObject, string propertyName)
123+
=> psObject.Properties[propertyName]?.Value as string ?? string.Empty;
124+
}
125+
}

test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,60 @@ await PsesLanguageClient
13001300
Assert.Equal("Get-ChildItem | Where-Object Name | ForEach-Object Name", expandAliasResult.Text);
13011301
}
13021302

1303+
[SkippableFact]
1304+
public async Task CanSendGetModuleRequestAsync()
1305+
{
1306+
Skip.If(PsesStdioLanguageServerProcessHost.RunningInConstrainedLanguageMode,
1307+
"The getModule request's script doesn't work in Constrained Language Mode.");
1308+
1309+
PSModuleMessage module =
1310+
await PsesLanguageClient
1311+
.SendRequest(
1312+
"powerShell/getModule",
1313+
new GetModuleParams
1314+
{
1315+
Name = "Microsoft.PowerShell.Management"
1316+
})
1317+
.Returning<PSModuleMessage>(CancellationToken.None);
1318+
1319+
Assert.NotNull(module);
1320+
Assert.Equal("Microsoft.PowerShell.Management", module.Name);
1321+
Assert.NotEmpty(module.Version);
1322+
Assert.NotEmpty(module.Path);
1323+
1324+
// Pinning to the resolved version should return that same version,
1325+
// exercising the handler's explicit-version branch.
1326+
PSModuleMessage pinned =
1327+
await PsesLanguageClient
1328+
.SendRequest(
1329+
"powerShell/getModule",
1330+
new GetModuleParams
1331+
{
1332+
Name = "Microsoft.PowerShell.Management",
1333+
Version = module.Version
1334+
})
1335+
.Returning<PSModuleMessage>(CancellationToken.None);
1336+
1337+
Assert.NotNull(pinned);
1338+
Assert.Equal(module.Version, pinned.Version);
1339+
}
1340+
1341+
[Fact]
1342+
public async Task CanSendGetModuleRequestForMissingModuleAsync()
1343+
{
1344+
PSModuleMessage module =
1345+
await PsesLanguageClient
1346+
.SendRequest(
1347+
"powerShell/getModule",
1348+
new GetModuleParams
1349+
{
1350+
Name = $"ThisModuleDoesNotExist-{Guid.NewGuid():N}"
1351+
})
1352+
.Returning<PSModuleMessage>(CancellationToken.None);
1353+
1354+
Assert.Null(module);
1355+
}
1356+
13031357
[Fact]
13041358
public async Task CanSendSemanticTokenRequestAsync()
13051359
{

0 commit comments

Comments
 (0)