From 3c45bc07e3e00d75ca77d23485c591a309fb44e5 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:20:47 -0700 Subject: [PATCH 1/6] Return help text from the `powerShell/showHelp` request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert `powerShell/showHelp` from a fire-and-forget notification into a request that returns `ShowHelpResult { HelpText }`, so the client can render help in a read-only editor pane (and the language model `get_help` tool can reuse the same path) instead of printing into the integrated console. The handler captures `Get-Help -Full | Out-String` and `.Trim()`s both ends — `Out-String` pads the output with a leading and trailing blank line, which looked wrong in the help pane and in tool output. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PowerShell/Handlers/ShowHelpHandler.cs | 76 +++++++++---------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/ShowHelpHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/ShowHelpHandler.cs index 43fcdfca2..4486d8512 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/ShowHelpHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/ShowHelpHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; using System.Management.Automation; using System.Threading; using System.Threading.Tasks; @@ -11,74 +12,67 @@ namespace Microsoft.PowerShell.EditorServices.Handlers { - [Serial, Method("powerShell/showHelp")] - internal interface IShowHelpHandler : IJsonRpcNotificationHandler { } + [Serial, Method("powerShell/showHelp", Direction.ClientToServer)] + internal interface IShowHelpHandler : IJsonRpcRequestHandler { } - internal class ShowHelpParams : IRequest + internal class ShowHelpParams : IRequest { public string Text { get; set; } } + internal class ShowHelpResult + { + public string HelpText { get; set; } + } + internal class ShowHelpHandler : IShowHelpHandler { private readonly IInternalPowerShellExecutionService _executionService; public ShowHelpHandler(IInternalPowerShellExecutionService executionService) => _executionService = executionService; - public async Task Handle(ShowHelpParams request, CancellationToken cancellationToken) + public async Task Handle(ShowHelpParams request, CancellationToken cancellationToken) { - // TODO: Refactor to not rerun the function definition every time. - const string CheckHelpScript = @" + // Resolves the command and returns its full help as a string so the + // client can display it (e.g. in an editor pane) or pass it to a + // language model tool. Returns a friendly message if the command + // cannot be found rather than throwing. + const string GetHelpScript = @" [System.Diagnostics.DebuggerHidden()] [CmdletBinding()] param ( [String]$CommandName ) - try { - $command = Microsoft.PowerShell.Core\Get-Command $CommandName -ErrorAction Stop - } catch [System.Management.Automation.CommandNotFoundException] { - $PSCmdlet.ThrowTerminatingError($PSItem) + $command = Microsoft.PowerShell.Core\Get-Command $CommandName -ErrorAction Ignore + if ($null -eq $command) { + return ""No command named '$CommandName' was found."" } - try { - $helpUri = [Microsoft.PowerShell.Commands.GetHelpCodeMethods]::GetHelpUri($command) - - $oldSslVersion = [System.Net.ServicePointManager]::SecurityProtocol - [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 - - # HEAD means we don't need the content itself back, just the response header - $status = (Microsoft.PowerShell.Utility\Invoke-WebRequest -Method Head -Uri $helpUri -TimeoutSec 5 -ErrorAction Stop).StatusCode - if ($status -lt 400) { - $null = Microsoft.PowerShell.Core\Get-Help $CommandName -Online - return - } - } catch { - # Ignore - we want to drop out to Get-Help -Full - } finally { - [System.Net.ServicePointManager]::SecurityProtocol = $oldSslVersion - } - - return Microsoft.PowerShell.Core\Get-Help $CommandName -Full + return (Microsoft.PowerShell.Core\Get-Help $CommandName -Full | Microsoft.PowerShell.Utility\Out-String) "; - string helpParams = request.Text; - if (string.IsNullOrEmpty(helpParams)) { helpParams = "Get-Help"; } + string commandName = request.Text; + if (string.IsNullOrEmpty(commandName)) { commandName = "Get-Help"; } - PSCommand checkHelpPSCommand = new PSCommand() - .AddScript(CheckHelpScript, useLocalScope: true) - .AddArgument(helpParams); + PSCommand getHelpCommand = new PSCommand() + .AddScript(GetHelpScript, useLocalScope: true) + .AddArgument(commandName); - // TODO: Rather than print the help in the console, we should send the string back - // to VSCode to display in a help pop-up (or similar) - await _executionService.ExecutePSCommandAsync( - checkHelpPSCommand, + IReadOnlyList results = await _executionService.ExecutePSCommandAsync( + getHelpCommand, cancellationToken, new PowerShellExecutionOptions { - RequiresForeground = true, - WriteOutputToHost = true, ThrowOnError = false }).ConfigureAwait(false); - return Unit.Value; + + // Get-Help piped through Out-String is padded with a leading blank + // line and trailing blank lines (a console-formatter artifact); trim + // both so the help pane and the language model tool get clean output. + string helpText = results is { Count: > 0 } + ? string.Concat(results).Trim() + : string.Empty; + + return new ShowHelpResult { HelpText = helpText }; } } } From 9cd797a451c7986a5a3941f8b3b6886b90e2492e Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:20:47 -0700 Subject: [PATCH 2/6] 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> --- .../PowerShell/Handlers/GetCommandHandler.cs | 161 +++++++++++++++++- 1 file changed, 157 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommandHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommandHandler.cs index a9f48ce52..98488eee3 100644 --- a/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommandHandler.cs +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetCommandHandler.cs @@ -2,7 +2,9 @@ // Licensed under the MIT License. using System.Collections.Generic; +using System.Linq; using System.Management.Automation; +using System.Management.Automation.Runspaces; using System.Threading; using System.Threading.Tasks; using MediatR; @@ -14,7 +16,37 @@ namespace Microsoft.PowerShell.EditorServices.Handlers [Serial, Method("powerShell/getCommand", Direction.ClientToServer)] internal interface IGetCommandHandler : IJsonRpcRequestHandler> { } - internal class GetCommandParams : IRequest> { } + internal class GetCommandParams : IRequest> + { + /// + /// An optional name (supports wildcards) to scope the returned commands. + /// When omitted, all commands are returned. + /// + public string Name { get; set; } + + /// + /// An optional module name (supports wildcards) to scope the returned + /// commands. When omitted, commands from all modules are returned. + /// + public string Module { get; set; } + + /// + /// When true, the expensive parameter and parameter-set metadata is not + /// resolved or returned. Callers that only need command names and modules + /// (such as the Command Explorer tree) should set this to avoid the large + /// serialization cost of the full command table. + /// + public bool ExcludeParameters { get; set; } + + /// + /// When true, module-less functions and scripts that PowerShell's default + /// session provides (e.g. cd.., prompt, TabExpansion2) are omitted. These + /// are interactive shell conveniences and engine plumbing rather than + /// commands a user authored or imported, so the Command Explorer hides them. + /// Module-provided commands (including built-in modules) are never affected. + /// + public bool ExcludeDefaultFunctions { get; set; } + } /// /// Describes the message to get the details for a single PowerShell Command @@ -24,6 +56,7 @@ internal class PSCommandMessage { public string Name { get; set; } public string ModuleName { get; set; } + public string ModuleVersion { get; set; } public string DefaultParameterSet { get; set; } public Dictionary Parameters { get; set; } public System.Collections.ObjectModel.ReadOnlyCollection ParameterSets { get; set; } @@ -39,11 +72,28 @@ public async Task> Handle(GetCommandParams request, Cance { PSCommand psCommand = new(); - // Executes the following: - // Get-Command -CommandType Function,Cmdlet,ExternalScript | Sort-Object -Property Name + // Executes the following, scoping by name and/or module when provided + // so we don't serialize the entire command table (which is expensive): + // Get-Command -CommandType Function,Cmdlet,ExternalScript [-Name ] [-Module ] | Sort-Object -Property Name psCommand .AddCommand(@"Microsoft.PowerShell.Core\Get-Command") - .AddParameter("CommandType", new[] { "Function", "Cmdlet", "ExternalScript" }) + .AddParameter("CommandType", new[] { "Function", "Cmdlet", "ExternalScript" }); + + if (!string.IsNullOrEmpty(request.Name)) + { + psCommand.AddParameter("Name", request.Name); + } + + if (!string.IsNullOrEmpty(request.Module)) + { + psCommand.AddParameter("Module", request.Module); + } + + // A name or module filter that matches nothing writes a non-terminating + // error; ignore it so we simply return an empty list instead. + psCommand.AddParameter("ErrorAction", "Ignore"); + + psCommand .AddCommand(@"Microsoft.PowerShell.Utility\Sort-Object") .AddParameter("Property", "Name"); @@ -54,6 +104,39 @@ public async Task> Handle(GetCommandParams request, Cance { foreach (CommandInfo command in result) { + // Skip commands injected by the editor's terminal integration + // (the PSES host's fake PSConsoleHostReadLine and VS Code's + // shell-integration helpers); they are implementation details, + // not real commands the user authored or imported. + if (IsEditorInjectedCommand(command)) + { + continue; + } + + // Optionally drop PowerShell's default-session shell functions + // (and the install's profile-resource script), which are + // module-less and not meaningful in the command list. + if (request.ExcludeDefaultFunctions + && IsDefaultSessionFunction(command)) + { + continue; + } + + // When only names/modules are requested, skip resolving the + // parameter metadata entirely. Accessing Parameters/ParameterSets + // forces PowerShell to compute (and we then serialize) the full + // metadata, which is the dominant cost for the whole command table. + if (request.ExcludeParameters) + { + commandList.Add(new PSCommandMessage + { + Name = command.Name, + ModuleName = command.ModuleName, + ModuleVersion = command.Version?.ToString() + }); + continue; + } + // Some info objects have a quicker way to get the DefaultParameterSet. These // are also the most likely to show up so win-win. string defaultParameterSet = null; @@ -84,6 +167,7 @@ public async Task> Handle(GetCommandParams request, Cance { Name = command.Name, ModuleName = command.ModuleName, + ModuleVersion = command.Version?.ToString(), Parameters = command.Parameters, ParameterSets = command.ParameterSets, DefaultParameterSet = defaultParameterSet @@ -93,5 +177,74 @@ public async Task> Handle(GetCommandParams request, Cance return commandList; } + + // Names of helper functions injected by VS Code's terminal shell + // integration script (shellIntegration.ps1), which the PSES host executes. + // These are editor plumbing rather than user- or module-provided commands. + private static readonly HashSet s_shellIntegrationFunctions = new(System.StringComparer.OrdinalIgnoreCase) + { + "__VSCode-Escape-Value", + "Set-MappedKeyHandler", + "Set-MappedKeyHandlers" + }; + + // Identifies commands injected by the editor's terminal integration that + // should not be surfaced as real commands. + private static bool IsEditorInjectedCommand(CommandInfo command) + { + if (command.CommandType != CommandTypes.Function) + { + return false; + } + + // The fake global PSConsoleHostReadLine function that the PSES host + // defines for terminal shell integration (see PsesInternalHost.cs) has + // no real version, whereas the genuine PSReadLine export always reports + // a real version, so that export is never matched here. + if (command.Name == "PSConsoleHostReadLine" + && (command.Version is null + || (command.Version.Major == 0 + && command.Version.Minor == 0 + && command.Version.Build <= 0 + && command.Version.Revision <= 0))) + { + return true; + } + + return s_shellIntegrationFunctions.Contains(command.Name); + } + + // The names of the functions that PowerShell's default session state + // provides (cd.., cd\, cd~, Clear-Host, exec, help, oss, Pause, prompt, + // TabExpansion2). Enumerated once from InitialSessionState so the list stays + // correct across PowerShell versions rather than being hard-coded. + private static readonly System.Lazy> s_defaultSessionFunctions = new(() => + new HashSet( + InitialSessionState.CreateDefault2().Commands + .OfType() + .Select(static entry => entry.Name), + System.StringComparer.OrdinalIgnoreCase)); + + // Identifies module-less functions and scripts that PowerShell's default + // session provides — interactive shell conveniences and engine plumbing that + // aren't meaningful in the command list. Only matches commands with no module, + // so a module-provided command (including built-in modules) is never affected. + private static bool IsDefaultSessionFunction(CommandInfo command) + { + if (!string.IsNullOrEmpty(command.ModuleName)) + { + return false; + } + + // The profile-resource script shipped alongside the PowerShell install + // (e.g. pwsh.profile.resource.ps1) is install plumbing, not a user script. + if (command.CommandType == CommandTypes.ExternalScript) + { + return command.Name.StartsWith("pwsh.profile.resource", System.StringComparison.OrdinalIgnoreCase); + } + + return command.CommandType == CommandTypes.Function + && s_defaultSessionFunctions.Value.Contains(command.Name); + } } } From 1e0c6d714aa7f6d92c213c0927f6210bb4fadb11 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:20:47 -0700 Subject: [PATCH 3/6] 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> --- .../Server/PsesLanguageServer.cs | 1 + .../PowerShell/Handlers/GetModuleHandler.cs | 125 ++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/PowerShellEditorServices/Services/PowerShell/Handlers/GetModuleHandler.cs diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 042e4e8fa..1ad6f3e76 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -121,6 +121,7 @@ public async Task StartAsync() .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .WithHandler() .WithHandler() .WithHandler() diff --git a/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetModuleHandler.cs b/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetModuleHandler.cs new file mode 100644 index 000000000..f974f0aa2 --- /dev/null +++ b/src/PowerShellEditorServices/Services/PowerShell/Handlers/GetModuleHandler.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Management.Automation; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using OmniSharp.Extensions.JsonRpc; +using Microsoft.PowerShell.EditorServices.Services.PowerShell; +using Microsoft.PowerShell.EditorServices.Services.PowerShell.Execution; + +namespace Microsoft.PowerShell.EditorServices.Handlers +{ + [Serial, Method("powerShell/getModule", Direction.ClientToServer)] + internal interface IGetModuleHandler : IJsonRpcRequestHandler { } + + internal class GetModuleParams : IRequest + { + /// + /// The name of the module to retrieve metadata for. + /// + public string Name { get; set; } + + /// + /// An optional specific version of the module. When omitted, the newest + /// available version is returned. + /// + public string Version { get; set; } + } + + /// + /// Describes the metadata for a single PowerShell module, used to populate + /// the Command Explorer's module tooltips. + /// + internal class PSModuleMessage + { + public string Name { get; set; } + public string Version { get; set; } + public string Description { get; set; } + public string Path { get; set; } + public string Author { get; set; } + public string CompanyName { get; set; } + public string ProjectUri { get; set; } + public string PowerShellVersion { get; set; } + } + + internal class GetModuleHandler : IGetModuleHandler + { + private readonly IInternalPowerShellExecutionService _executionService; + + public GetModuleHandler(IInternalPowerShellExecutionService executionService) => _executionService = executionService; + + public async Task Handle(GetModuleParams request, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(request.Name)) + { + return null; + } + + // Resolve a module's metadata from the available modules, pinning to a + // specific version when requested and otherwise taking the newest. + const string GetModuleScript = @" + [System.Diagnostics.DebuggerHidden()] + [CmdletBinding()] + param ( + [String]$Name, + [String]$Version + ) + $modules = Microsoft.PowerShell.Core\Get-Module -ListAvailable -Name $Name -ErrorAction Ignore + if ($Version) { + $modules = $modules | Microsoft.PowerShell.Core\Where-Object { $_.Version.ToString() -eq $Version } + } + $module = $modules | Microsoft.PowerShell.Utility\Sort-Object Version -Descending | Microsoft.PowerShell.Utility\Select-Object -First 1 + if ($null -eq $module) { + return + } + [PSCustomObject]@{ + Name = $module.Name + Version = $module.Version.ToString() + Description = $module.Description + Path = $module.Path + Author = $module.Author + CompanyName = $module.CompanyName + ProjectUri = if ($module.ProjectUri) { $module.ProjectUri.ToString() } else { '' } + PowerShellVersion = if ($module.PowerShellVersion) { $module.PowerShellVersion.ToString() } else { '' } + } + "; + + PSCommand getModuleCommand = new PSCommand() + .AddScript(GetModuleScript, useLocalScope: true) + .AddParameter("Name", request.Name) + .AddParameter("Version", request.Version); + + IReadOnlyList results = await _executionService.ExecutePSCommandAsync( + getModuleCommand, + cancellationToken, + new PowerShellExecutionOptions + { + ThrowOnError = false + }).ConfigureAwait(false); + + PSObject result = results is { Count: > 0 } ? results[0] : null; + if (result is null) + { + return null; + } + + return new PSModuleMessage + { + Name = GetPropertyString(result, "Name"), + Version = GetPropertyString(result, "Version"), + Description = GetPropertyString(result, "Description"), + Path = GetPropertyString(result, "Path"), + Author = GetPropertyString(result, "Author"), + CompanyName = GetPropertyString(result, "CompanyName"), + ProjectUri = GetPropertyString(result, "ProjectUri"), + PowerShellVersion = GetPropertyString(result, "PowerShellVersion") + }; + } + + private static string GetPropertyString(PSObject psObject, string propertyName) + => psObject.Properties[propertyName]?.Value as string ?? string.Empty; + } +} From b2dbcb7ec2693556f58cce674cad8548115d234a Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 9 Jun 2026 15:20:47 -0700 Subject: [PATCH 4/6] Add Copilot instructions documenting build and test Capture how to build and test the project with `dotnet` directly (faster, no extra modules) versus `Invoke-Build` (needed for assembling the full module and the complete CI suite), so future Copilot sessions don't have to rediscover it. Drafted by Copilot (Claude Opus 4.8). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 110 ++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..2f79e603e --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,110 @@ +# Copilot Instructions for PowerShell Editor Services + +## Build & Test + +Requires .NET SDK 8.0+. Use `dotnet` directly for building and testing — it's faster and +requires no extra tooling. The `Invoke-Build` script requires the `InvokeBuild` and `platyPS` +PowerShell modules (platyPS is `#Requires`'d at the top, so the whole script fails without it), +and is mainly needed to assemble the full PowerShell module for release. + +```powershell +# Build (run both; Hosting depends on the core library) +dotnet publish src/PowerShellEditorServices/PowerShellEditorServices.csproj -f netstandard2.0 +dotnet publish src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj -f net8.0 + +# Run all unit tests +dotnet test test/PowerShellEditorServices.Test/ --framework net8.0 + +# Run a single test by name +dotnet test test/PowerShellEditorServices.Test/ --framework net8.0 --filter "FullyQualifiedName~CompletesCommandInFile" + +# Run tests by trait category +dotnet test test/PowerShellEditorServices.Test/ --framework net8.0 --filter "Category=Completions" + +# Run E2E tests +dotnet test test/PowerShellEditorServices.Test.E2E/ --framework net8.0 +``` + +For assembling the full module or running the complete CI suite (including Windows PowerShell +5.1 targets), use `Invoke-Build` with the `InvokeBuild` and `platyPS` modules installed: + +```powershell +Invoke-Build Build # full build + module assembly + help generation +Invoke-Build TestPS74 # unit tests via build script +Invoke-Build TestE2EPwsh # E2E tests via build script +Invoke-Build Build -Configuration Release # required before PRs (enforces XML doc comments) +``` + +`src/PowerShellEditorServices.Hosting/BuildInfo.cs` is auto-generated by the build script and +git-ignored for changes. Do not edit it manually. + +## Architecture + +PowerShell Editor Services (PSES) is a **Language Server Protocol (LSP)** and **Debug Adapter +Protocol (DAP)** server for PowerShell, consumed by VS Code and other editors. + +### Projects + +- **`src/PowerShellEditorServices`** (`netstandard2.0`) — Core library containing all LSP/DAP + handlers, services, and the PowerShell execution engine. Namespace: + `Microsoft.PowerShell.EditorServices`. +- **`src/PowerShellEditorServices.Hosting`** (`net8.0`, `net462`) — Entry point layer that loads + PSES into a PowerShell process via `StartEditorServicesCommand`. Uses a custom + `AssemblyLoadContext` (`PsesLoadContext`) on .NET Core to isolate dependencies. +- **`module/PowerShellEditorServices/`** — The shipped PowerShell module. The build assembles + compiled binaries into `bin/Core/` (net8.0) and `bin/Desktop/` (net462). The module manifest + loads the appropriate DLL based on PowerShell edition. + +### Key Services (registered in `PsesServiceCollectionExtensions`) + +- **`PsesInternalHost`** — The central PowerShell execution host. Also implements + `IRunspaceContext` and `IInternalPowerShellExecutionService`. +- **`WorkspaceService`** — Manages open documents and workspace files. +- **`SymbolsService`** — Provides symbol navigation (go-to-definition, find references). +- **`AnalysisService`** — Integrates PSScriptAnalyzer for real-time diagnostics. +- **`ConfigurationService`** — Manages editor/client settings. +- **`ExtensionService`** — Supports the `$psEditor` API for editor extensions. + +### LSP/DAP Handler Pattern + +Handlers live under `Services//Handlers/` and follow a consistent pattern: + +- Class name: `PsesHandler`, marked `internal` +- Inherits from an OmniSharp base class (e.g., `CompletionHandlerBase`, `HoverHandlerBase`) +- Dependencies injected via constructor (`ILoggerFactory`, services) +- Overrides `CreateRegistrationOptions()` and `Handle()` +- Uses `LspUtils.PowerShellDocumentSelector` for document registration + +### Server Setup + +- `PsesLanguageServer` — Configures and runs the LSP server using OmniSharp +- `PsesDebugServer` — Configures and runs the DAP server +- Both use `Microsoft.Extensions.DependencyInjection` for service registration + +## Conventions + +### C# Style + +- All files require the copyright header: `// Copyright (c) Microsoft Corporation.` / + `// Licensed under the MIT License.` +- `.editorconfig` enforces many rules as **errors**, including unused variables, async/threading + rules (`VSTHRD*`), and modern C# idioms (pattern matching, null checks, expression bodies). +- Roslynator analyzers are enabled for formatting and code quality. +- Use `Microsoft.Extensions.Logging` (`ILogger` via `ILoggerFactory`) for all logging. + +### Testing + +- **Framework:** xUnit with `Xunit.SkippableFact` for conditionally skipped tests. +- **Host setup:** Use `PsesHostFactory.Create(loggerFactory)` to get an isolated + `PsesInternalHost` for testing. Tests implement `IAsyncLifetime` for async setup/teardown. +- **Traits:** Tests use `[Trait("Category", "...")]` for filtering (e.g., `"Completions"`, + `"Symbols"`). +- **Fixtures:** Test PowerShell scripts live in `test/PowerShellEditorServices.Test/Fixtures/`. +- **E2E tests** are in a separate project (`PowerShellEditorServices.Test.E2E`) and test the + full LSP client-server interaction. + +### Multi-targeting + +The core library targets `netstandard2.0` for compatibility with both .NET Core and .NET +Framework. The hosting project and tests dual-target `net8.0` and `net462` (Windows PowerShell +5.1). Non-Windows platforms skip `net462` targets. From 3f47c4e310ba569fdd1963c1e0bbf97580a51601 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 01:05:55 +0000 Subject: [PATCH 5/6] Add E2E tests for powerShell/getModule request --- .../LanguageServerProtocolMessageTests.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index 155c23a3a..b2c9c1945 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -1274,6 +1274,40 @@ await PsesLanguageClient Assert.Equal("Get-ChildItem", expandAliasResult.Text); } + [Fact] + public async Task CanSendGetModuleRequestAsync() + { + PSModuleMessage module = + await PsesLanguageClient + .SendRequest( + "powerShell/getModule", + new GetModuleParams + { + Name = "Microsoft.PowerShell.Management" + }) + .Returning(CancellationToken.None); + + Assert.NotNull(module); + Assert.Equal("Microsoft.PowerShell.Management", module.Name); + Assert.NotEmpty(module.Version); + } + + [Fact] + public async Task CanSendGetModuleRequestForMissingModuleAsync() + { + PSModuleMessage module = + await PsesLanguageClient + .SendRequest( + "powerShell/getModule", + new GetModuleParams + { + Name = "ThisModuleDoesNotExist-" + Guid.NewGuid().ToString("N") + }) + .Returning(CancellationToken.None); + + Assert.Null(module); + } + [Fact] public async Task CanSendSemanticTokenRequestAsync() { From a8b603f778253cee6b1c358d4bbaed2e9a9f17d1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 01:06:32 +0000 Subject: [PATCH 6/6] Use string interpolation for missing module name in test --- .../LanguageServerProtocolMessageTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs index b2c9c1945..585a01e6f 100644 --- a/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs +++ b/test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs @@ -1301,7 +1301,7 @@ await PsesLanguageClient "powerShell/getModule", new GetModuleParams { - Name = "ThisModuleDoesNotExist-" + Guid.NewGuid().ToString("N") + Name = $"ThisModuleDoesNotExist-{Guid.NewGuid():N}" }) .Returning(CancellationToken.None);