Skip to content

Commit 46df818

Browse files
andyleejordandkattan
authored andcommitted
Removed new parameter and switched instead on whether Stdio is in use determine whether or not to use the NullPSHostUI
1 parent 1905151 commit 46df818

File tree

8 files changed

+147
-25
lines changed

8 files changed

+147
-25
lines changed

src/PowerShellEditorServices.Hosting/Commands/StartEditorServicesCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ private EditorServicesConfig CreateConfigObject()
351351
FeatureFlags = FeatureFlags,
352352
LogLevel = LogLevel,
353353
ConsoleRepl = GetReplKind(),
354+
UseNullPSHostUI = Stdio,
354355
AdditionalModules = AdditionalModules,
355356
LanguageServiceTransport = GetLanguageServiceTransport(),
356357
DebugServiceTransport = GetDebugServiceTransport(),

src/PowerShellEditorServices.Hosting/Configuration/EditorServicesConfig.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ public EditorServicesConfig(
8989
/// </summary>
9090
public ConsoleReplKind ConsoleRepl { get; set; } = ConsoleReplKind.None;
9191

92+
/// <summary>
93+
/// Will suppress messages to PSHost (to prevent Stdio clobbering)
94+
/// </summary>
95+
public bool UseNullPSHostUI { get; set; }
96+
9297
/// <summary>
9398
/// The minimum log level to log events with.
9499
/// </summary>

src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using Microsoft.PowerShell.EditorServices.Server;
45
using System;
56
using System.Collections.Generic;
67
using System.IO;
78
using System.Threading.Tasks;
8-
using Microsoft.PowerShell.EditorServices.Server;
99

1010
namespace Microsoft.PowerShell.EditorServices.Hosting
1111
{
@@ -289,6 +289,7 @@ private HostStartupInfo CreateHostStartupInfo()
289289
_config.LogPath,
290290
(int)_config.LogLevel,
291291
consoleReplEnabled: _config.ConsoleRepl != ConsoleReplKind.None,
292+
useNullPSHostUI: _config.UseNullPSHostUI,
292293
usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine,
293294
bundledModulePath: _config.BundledModulePath);
294295
}

src/PowerShellEditorServices/Hosting/HostStartupInfo.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ public sealed class HostStartupInfo
7676
/// </summary>
7777
public bool ConsoleReplEnabled { get; }
7878

79+
/// <summary>
80+
/// True if we want to suppress messages to PSHost (to prevent Stdio clobbering)
81+
/// </summary>
82+
public bool UseNullPSHostUI { get; }
83+
7984
/// <summary>
8085
/// If true, the legacy PSES readline implementation will be used. Otherwise PSReadLine will be used.
8186
/// If the console REPL is not enabled, this setting will be ignored.
@@ -154,7 +159,8 @@ public HostStartupInfo(
154159
int logLevel,
155160
bool consoleReplEnabled,
156161
bool usesLegacyReadLine,
157-
string bundledModulePath)
162+
string bundledModulePath,
163+
bool useNullPSHostUI)
158164
{
159165
Name = name ?? DefaultHostName;
160166
ProfileId = profileId ?? DefaultHostProfileId;
@@ -177,6 +183,7 @@ public HostStartupInfo(
177183
"..",
178184
"..",
179185
".."));
186+
UseNullPSHostUI = useNullPSHostUI;
180187
}
181188

182189
#endregion

src/PowerShellEditorServices/Services/Analysis/PssaCmdletAnalysisEngine.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,17 @@ private PowerShellResult InvokePowerShell(PSCommand command)
292292
catch (CmdletInvocationException ex)
293293
{
294294
// We do not want to crash EditorServices for exceptions caused by cmdlet invocation.
295-
// Two main reasons that cause the exception are:
295+
// The main reasons that cause the exception are:
296296
// * PSCmdlet.WriteOutput being called from another thread than Begin/Process
297297
// * CompositionContainer.ComposeParts complaining that "...Only one batch can be composed at a time"
298-
_logger.LogError(ex.Message);
298+
// * PSScriptAnalyzer not being able to find its PSScriptAnalyzer.psd1 because we are hosted by an Assembly other than pwsh.exe
299+
string message = ex.Message;
300+
if (!string.IsNullOrEmpty(ex.ErrorRecord.FullyQualifiedErrorId))
301+
{
302+
// Microsoft.PowerShell.EditorServices.Services.Analysis.PssaCmdletAnalysisEngine: Exception of type 'System.Exception' was thrown. |
303+
message += $" | {ex.ErrorRecord.FullyQualifiedErrorId}";
304+
}
305+
_logger.LogError(message);
299306
}
300307

301308
return result;

src/PowerShellEditorServices/Services/PowerShell/Host/PsesInternalHost.cs

Lines changed: 116 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@ public PsesInternalHost(
193193
Version = hostInfo.Version;
194194

195195
DebugContext = new PowerShellDebugContext(loggerFactory, this);
196-
UI = hostInfo.ConsoleReplEnabled
197-
? new EditorServicesConsolePSHostUserInterface(loggerFactory, hostInfo.PSHost.UI)
198-
: new NullPSHostUI();
196+
UI = hostInfo.UseNullPSHostUI
197+
? new NullPSHostUI()
198+
: new EditorServicesConsolePSHostUserInterface(loggerFactory, hostInfo.PSHost.UI);
199199
}
200200

201201
public override CultureInfo CurrentCulture => _hostInfo.PSHost.CurrentCulture;
@@ -583,7 +583,7 @@ internal Task LoadHostProfilesAsync(CancellationToken cancellationToken)
583583

584584
private Task EnableShellIntegrationAsync(CancellationToken cancellationToken)
585585
{
586-
// Imported on 01/03/23 from
586+
// Imported on 01/03/24 from
587587
// https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminal/browser/media/shellIntegration.ps1
588588
// with quotes escaped, `__VSCodeOriginalPSConsoleHostReadLine` removed (as it's done
589589
// in our own ReadLine function), and `[Console]::Write` replaced with `Write-Host`.
@@ -602,42 +602,74 @@ private Task EnableShellIntegrationAsync(CancellationToken cancellationToken)
602602
603603
$Global:__LastHistoryId = -1
604604
605+
# Store the nonce in script scope and unset the global
606+
$Nonce = $env:VSCODE_NONCE
607+
$env:VSCODE_NONCE = $null
608+
609+
if ($env:VSCODE_ENV_REPLACE) {
610+
$Split = $env:VSCODE_ENV_REPLACE.Split("":"")
611+
foreach ($Item in $Split) {
612+
$Inner = $Item.Split('=')
613+
[Environment]::SetEnvironmentVariable($Inner[0], $Inner[1].Replace('\x3a', ':'))
614+
}
615+
$env:VSCODE_ENV_REPLACE = $null
616+
}
617+
if ($env:VSCODE_ENV_PREPEND) {
618+
$Split = $env:VSCODE_ENV_PREPEND.Split("":"")
619+
foreach ($Item in $Split) {
620+
$Inner = $Item.Split('=')
621+
[Environment]::SetEnvironmentVariable($Inner[0], $Inner[1].Replace('\x3a', ':') + [Environment]::GetEnvironmentVariable($Inner[0]))
622+
}
623+
$env:VSCODE_ENV_PREPEND = $null
624+
}
625+
if ($env:VSCODE_ENV_APPEND) {
626+
$Split = $env:VSCODE_ENV_APPEND.Split("":"")
627+
foreach ($Item in $Split) {
628+
$Inner = $Item.Split('=')
629+
[Environment]::SetEnvironmentVariable($Inner[0], [Environment]::GetEnvironmentVariable($Inner[0]) + $Inner[1].Replace('\x3a', ':'))
630+
}
631+
$env:VSCODE_ENV_APPEND = $null
632+
}
633+
605634
function Global:__VSCode-Escape-Value([string]$value) {
606635
# NOTE: In PowerShell v6.1+, this can be written `$value -replace '…', { … }` instead of `[regex]::Replace`.
607636
# Replace any non-alphanumeric characters.
608637
[regex]::Replace($value, '[\\\n;]', { param($match)
609-
# Encode the (ascii) matches as `\x<hex>`
610-
-Join (
611-
[System.Text.Encoding]::UTF8.GetBytes($match.Value) | ForEach-Object { '\x{0:x2}' -f $_ }
612-
)
613-
})
638+
# Encode the (ascii) matches as `\x<hex>`
639+
-Join (
640+
[System.Text.Encoding]::UTF8.GetBytes($match.Value) | ForEach-Object { '\x{0:x2}' -f $_ }
641+
)
642+
})
614643
}
615644
616645
function Global:Prompt() {
646+
$FakeCode = [int]!$global:?
617647
# NOTE: We disable strict mode for the scope of this function because it unhelpfully throws an
618648
# error when $LastHistoryEntry is null, and is not otherwise useful.
619649
Set-StrictMode -Off
620-
$FakeCode = [int]!$global:?
621650
$LastHistoryEntry = Get-History -Count 1
622651
# Skip finishing the command if the first command has not yet started
623652
if ($Global:__LastHistoryId -ne -1) {
624653
if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) {
625654
# Don't provide a command line or exit code if there was no history entry (eg. ctrl+c, enter on no command)
626-
$Result = ""$([char]0x1b)]633;E`a""
655+
$Result = ""$([char]0x1b)]633;E`a""
627656
$Result += ""$([char]0x1b)]633;D`a""
628-
} else {
657+
}
658+
else {
629659
# Command finished command line
630-
# OSC 633 ; A ; <CommandLine?> ST
631-
$Result = ""$([char]0x1b)]633;E;""
660+
# OSC 633 ; E ; <CommandLine?> ; <Nonce?> ST
661+
$Result = ""$([char]0x1b)]633;E;""
632662
# Sanitize the command line to ensure it can get transferred to the terminal and can be parsed
633663
# correctly. This isn't entirely safe but good for most cases, it's important for the Pt parameter
634664
# to only be composed of _printable_ characters as per the spec.
635665
if ($LastHistoryEntry.CommandLine) {
636666
$CommandLine = $LastHistoryEntry.CommandLine
637-
} else {
667+
}
668+
else {
638669
$CommandLine = """"
639670
}
640671
$Result += $(__VSCode-Escape-Value $CommandLine)
672+
$Result += "";$Nonce""
641673
$Result += ""`a""
642674
# Command finished exit code
643675
# OSC 633 ; D [; <ExitCode>] ST
@@ -649,7 +681,7 @@ private Task EnableShellIntegrationAsync(CancellationToken cancellationToken)
649681
$Result += ""$([char]0x1b)]633;A`a""
650682
# Current working directory
651683
# OSC 633 ; <Property>=<Value> ST
652-
$Result += if($pwd.Provider.Name -eq 'FileSystem'){""$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a""}
684+
$Result += if ($pwd.Provider.Name -eq 'FileSystem') { ""$([char]0x1b)]633;P;Cwd=$(__VSCode-Escape-Value $pwd.ProviderPath)`a"" }
653685
# Before running the original prompt, put $? back to what it was:
654686
if ($FakeCode -ne 0) {
655687
Write-Error ""failure"" -ea ignore
@@ -664,28 +696,91 @@ private Task EnableShellIntegrationAsync(CancellationToken cancellationToken)
664696
665697
# Set IsWindows property
666698
if ($PSVersionTable.PSVersion -lt ""6.0"") {
667-
[Console]::Write(""$([char]0x1b)]633;P;IsWindows=$true`a"")
668-
} else {
669-
[Console]::Write(""$([char]0x1b)]633;P;IsWindows=$IsWindows`a"")
699+
# Windows PowerShell is only available on Windows
700+
Write-Host -NoNewLine ""$([char]0x1b)]633;P;IsWindows=$true`a""
701+
}
702+
else {
703+
Write-Host -NoNewLine ""$([char]0x1b)]633;P;IsWindows=$IsWindows`a""
670704
}
671705
672706
# Set always on key handlers which map to default VS Code keybindings
673707
function Set-MappedKeyHandler {
674708
param ([string[]] $Chord, [string[]]$Sequence)
675-
$Handler = $(Get-PSReadLineKeyHandler -Chord $Chord | Select-Object -First 1)
709+
try {
710+
$Handler = Get-PSReadLineKeyHandler -Chord $Chord | Select-Object -First 1
711+
}
712+
catch [System.Management.Automation.ParameterBindingException] {
713+
# PowerShell 5.1 ships with PSReadLine 2.0.0 which does not have -Chord,
714+
# so we check what's bound and filter it.
715+
$Handler = Get-PSReadLineKeyHandler -Bound | Where-Object -FilterScript { $_.Key -eq $Chord } | Select-Object -First 1
716+
}
676717
if ($Handler) {
677718
Set-PSReadLineKeyHandler -Chord $Sequence -Function $Handler.Function
678719
}
679720
}
680721
722+
$Global:__VSCodeHaltCompletions = $false
681723
function Set-MappedKeyHandlers {
682724
Set-MappedKeyHandler -Chord Ctrl+Spacebar -Sequence 'F12,a'
683725
Set-MappedKeyHandler -Chord Alt+Spacebar -Sequence 'F12,b'
684726
Set-MappedKeyHandler -Chord Shift+Enter -Sequence 'F12,c'
685727
Set-MappedKeyHandler -Chord Shift+End -Sequence 'F12,d'
728+
729+
# Conditionally enable suggestions
730+
if ($env:VSCODE_SUGGEST -eq '1') {
731+
Remove-Item Env:VSCODE_SUGGEST
732+
733+
# VS Code send completions request (may override Ctrl+Spacebar)
734+
Set-PSReadLineKeyHandler -Chord 'F12,e' -ScriptBlock {
735+
Send-Completions
736+
}
737+
738+
# Suggest trigger characters
739+
Set-PSReadLineKeyHandler -Chord ""-"" -ScriptBlock {
740+
[Microsoft.PowerShell.PSConsoleReadLine]::Insert(""-"")
741+
if (!$Global:__VSCodeHaltCompletions) {
742+
Send-Completions
743+
}
744+
}
745+
746+
Set-PSReadLineKeyHandler -Chord 'F12,y' -ScriptBlock {
747+
$Global:__VSCodeHaltCompletions = $true
748+
}
749+
750+
Set-PSReadLineKeyHandler -Chord 'F12,z' -ScriptBlock {
751+
$Global:__VSCodeHaltCompletions = $false
752+
}
753+
}
754+
}
755+
756+
function Send-Completions {
757+
$commandLine = """"
758+
$cursorIndex = 0
759+
# TODO: Since fuzzy matching exists, should completions be provided only for character after the
760+
# last space and then filter on the client side? That would let you trigger ctrl+space
761+
# anywhere on a word and have full completions available
762+
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$commandLine, [ref]$cursorIndex)
763+
$completionPrefix = $commandLine
764+
765+
# Get completions
766+
$result = ""`e]633;Completions""
767+
if ($completionPrefix.Length -gt 0) {
768+
# Get and send completions
769+
$completions = TabExpansion2 -inputScript $completionPrefix -cursorColumn $cursorIndex
770+
if ($null -ne $completions.CompletionMatches) {
771+
$result += "";$($completions.ReplacementIndex);$($completions.ReplacementLength);$($cursorIndex);""
772+
$result += $completions.CompletionMatches | ConvertTo-Json -Compress
773+
}
774+
}
775+
$result += ""`a""
776+
777+
Write-Host -NoNewLine $result
686778
}
687779
688-
Set-MappedKeyHandlers
780+
# Register key handlers if PSReadLine is available
781+
if (Get-Module -Name PSReadLine) {
782+
Set-MappedKeyHandlers
783+
}
689784
";
690785

691786
return ExecutePSCommandAsync(new PSCommand().AddScript(shellIntegrationScript), cancellationToken);

src/PowerShellEditorServices/Services/PowerShell/Utility/PowerShellExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace Microsoft.PowerShell.EditorServices.Services.PowerShell.Utility
1414
{
15+
using System.Collections;
1516
using System.Management.Automation;
1617

1718
internal static class PowerShellExtensions
@@ -73,6 +74,10 @@ public static Collection<TResult> InvokeAndClear<TResult>(this PowerShell pwsh,
7374
{
7475
try
7576
{
77+
if (pwsh.Runspace.SessionStateProxy.PSVariable.GetValue("error") is ArrayList errors)
78+
{
79+
errors.Clear();
80+
}
7681
return pwsh.Invoke<TResult>(input: null, invocationSettings);
7782
}
7883
finally

test/PowerShellEditorServices.Test/PsesHostFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public static PsesInternalHost Create(ILoggerFactory loggerFactory, bool loadPro
5757
logLevel: (int)LogLevel.None,
5858
consoleReplEnabled: false,
5959
usesLegacyReadLine: false,
60+
useNullPSHostUI: true, // Preserve previous functionality
6061
bundledModulePath: BundledModulePath);
6162

6263
PsesInternalHost psesHost = new(loggerFactory, null, testHostDetails);

0 commit comments

Comments
 (0)