Skip to content

Commit bfce295

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 9cd797a commit bfce295

3 files changed

Lines changed: 177 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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,57 @@ await PsesLanguageClient
12741274
Assert.Equal("Get-ChildItem", expandAliasResult.Text);
12751275
}
12761276

1277+
[Fact]
1278+
public async Task CanSendGetModuleRequestAsync()
1279+
{
1280+
PSModuleMessage module =
1281+
await PsesLanguageClient
1282+
.SendRequest(
1283+
"powerShell/getModule",
1284+
new GetModuleParams
1285+
{
1286+
Name = "Microsoft.PowerShell.Management"
1287+
})
1288+
.Returning<PSModuleMessage>(CancellationToken.None);
1289+
1290+
Assert.NotNull(module);
1291+
Assert.Equal("Microsoft.PowerShell.Management", module.Name);
1292+
Assert.NotEmpty(module.Version);
1293+
Assert.NotEmpty(module.Path);
1294+
1295+
// Pinning to the resolved version should return that same version,
1296+
// exercising the handler's explicit-version branch.
1297+
PSModuleMessage pinned =
1298+
await PsesLanguageClient
1299+
.SendRequest(
1300+
"powerShell/getModule",
1301+
new GetModuleParams
1302+
{
1303+
Name = "Microsoft.PowerShell.Management",
1304+
Version = module.Version
1305+
})
1306+
.Returning<PSModuleMessage>(CancellationToken.None);
1307+
1308+
Assert.NotNull(pinned);
1309+
Assert.Equal(module.Version, pinned.Version);
1310+
}
1311+
1312+
[Fact]
1313+
public async Task CanSendGetModuleRequestForMissingModuleAsync()
1314+
{
1315+
PSModuleMessage module =
1316+
await PsesLanguageClient
1317+
.SendRequest(
1318+
"powerShell/getModule",
1319+
new GetModuleParams
1320+
{
1321+
Name = $"ThisModuleDoesNotExist-{Guid.NewGuid():N}"
1322+
})
1323+
.Returning<PSModuleMessage>(CancellationToken.None);
1324+
1325+
Assert.Null(module);
1326+
}
1327+
12771328
[Fact]
12781329
public async Task CanSendSemanticTokenRequestAsync()
12791330
{

0 commit comments

Comments
 (0)