diff --git a/.gitignore b/.gitignore index 8a1e32649..fb0d46c89 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,6 @@ PSReadline.zip [Oo]bj/ .ionide/ -# VSCode directories that are not at the repository root -/**/.vscode/ - # mstest test results TestResults FakesAssemblies/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 2d929fb4b..c01006234 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,7 +4,24 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - + { + "name": "Launch PSReadLine", + "type": "coreclr", + "request": "launch", + "program": "pwsh", + "args": [ + "-NonInteractive", + "-NoProfile", + "-NoExit", + "-Command", + "Import-Module '${workspaceFolder}/PSReadLine/bin/Debug/netstandard2.0/PSReadLine.psd1'" + ], + "console": "integratedTerminal", + "justMyCode": false, + "suppressJITOptimizations": true, + "enableStepFiltering": false, + "preLaunchTask": "Build", + }, { "name": ".NET Core Attach", "type": "coreclr", @@ -16,4 +33,4 @@ } } ] -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..04326ed55 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,76 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Bootstrap", + "type": "shell", + "command": "pwsh", + "args": [ + "./build.ps1", + "-Bootstrap" + ], + "group": "build", + "detail": "Install build prerequisites (InvokeBuild, .NET SDK)" + }, + { + "label": "Build", + "type": "shell", + "command": "pwsh", + "args": [ + "./build.ps1" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [ + "$msCompile" + ], + "detail": "Build with user-selected configuration" + }, + { + "label": "Run Tests", + "type": "shell", + "command": "pwsh", + "args": [ + "./build.ps1", + "-Test", + "-Framework", + "${input:framework}" + ], + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "focus": true, + "panel": "dedicated", + "clear": true + }, + "detail": "Run unit tests with selected framework" + }, + { + "label": "Clean", + "type": "shell", + "command": "pwsh", + "args": [ + "./build.ps1", + "-Clean" + ], + "group": "build", + "detail": "Clean build artifacts" + } + ], + "inputs": [ + { + "id": "framework", + "description": "Target Framework", + "type": "pickString", + "options": [ + "net472", + "net6.0" + ], + "default": "net6.0" + } + ] +} diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index f3d277752..919f92154 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,14 @@ +### [2.4.3-beta3] - 2025-07-23 + +- Allow accepting the current input automatically from within an `OnIdle` event handler (#4830) +- Add VS Code tasks and debug config (#4834, #4855) +- Add bound check for the cursor top value to `InvokePrompt` (#4791) (Thanks @jftkcs!) +- Fix typo in `SamplePSReadLineProfile.ps1` (#4725) (Thanks @mahir-cadirci!) +- Fix line ending and cache some reflection operations (#4709) +- Improve test reliability by making sure the PSReadLine one-time initialization is done (#4686) (Thanks @springcomp!) + +[2.4.3-beta3]: https://github.com/PowerShell/PSReadLine/compare/v2.4.2-beta2...v2.4.3-beta3 + ### [2.4.2-beta2] - 2025-04-16 - Add a private field to indicate if PSReadLine is initialized and ready (#4706) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 1c8090f84..01576b652 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -5,9 +5,9 @@ Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine $(NoWarn);CA1416 - 2.4.2.0 - 2.4.2 - 2.4.2-beta2 + 2.4.3.0 + 2.4.3 + 2.4.3-beta3 true netstandard2.0 true diff --git a/PSReadLine/PSReadLine.psd1 b/PSReadLine/PSReadLine.psd1 index 543d3f442..53d70cfd3 100644 --- a/PSReadLine/PSReadLine.psd1 +++ b/PSReadLine/PSReadLine.psd1 @@ -1,7 +1,7 @@ @{ RootModule = 'PSReadLine.psm1' NestedModules = @("Microsoft.PowerShell.PSReadLine.dll") -ModuleVersion = '2.4.2' +ModuleVersion = '2.4.3' GUID = '5714753b-2afd-4492-a5fd-01d9e2cff8b5' Author = 'Microsoft Corporation' CompanyName = 'Microsoft Corporation' diff --git a/PSReadLine/PSReadLine.sln b/PSReadLine/PSReadLine.sln index 3ce7ce89f..866d96120 100644 --- a/PSReadLine/PSReadLine.sln +++ b/PSReadLine/PSReadLine.sln @@ -1,53 +1,61 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.12 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSReadLine", "PSReadLine.csproj", "{615788CB-1B9A-4B34-97B3-4608686E59CA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyfill", "..\Polyfill\Polyfill.csproj", "{DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MockPSConsole", "..\MockPSConsole\MockPSConsole.csproj", "{08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSReadLine.Tests", "..\test\PSReadLine.Tests.csproj", "{8ED51D01-158C-4B29-824A-35B9B861E45A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - CodeCoverage|Any CPU = CodeCoverage|Any CPU - Debug|Any CPU = Debug|Any CPU - Linux|Any CPU = Linux|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {615788CB-1B9A-4B34-97B3-4608686E59CA}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.Linux|Any CPU.ActiveCfg = Release|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.Linux|Any CPU.Build.0 = Release|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {615788CB-1B9A-4B34-97B3-4608686E59CA}.Release|Any CPU.Build.0 = Release|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Linux|Any CPU.ActiveCfg = Release|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Linux|Any CPU.Build.0 = Release|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Release|Any CPU.Build.0 = Release|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.Linux|Any CPU.ActiveCfg = Release|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.Linux|Any CPU.Build.0 = Release|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8ED51D01-158C-4B29-824A-35B9B861E45A}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {EA13C704-483F-4CE4-A3FB-8F79295F1071} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSReadLine", "PSReadLine.csproj", "{615788CB-1B9A-4B34-97B3-4608686E59CA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyfill", "..\Polyfill\Polyfill.csproj", "{DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MockPSConsole", "..\MockPSConsole\MockPSConsole.csproj", "{08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSReadLine.Tests", "..\test\PSReadLine.Tests.csproj", "{8ED51D01-158C-4B29-824A-35B9B861E45A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + CodeCoverage|Any CPU = CodeCoverage|Any CPU + Debug|Any CPU = Debug|Any CPU + Linux|Any CPU = Linux|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {615788CB-1B9A-4B34-97B3-4608686E59CA}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Linux|Any CPU.ActiveCfg = Release|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Linux|Any CPU.Build.0 = Release|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {615788CB-1B9A-4B34-97B3-4608686E59CA}.Release|Any CPU.Build.0 = Release|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.Linux|Any CPU.ActiveCfg = Release|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.Linux|Any CPU.Build.0 = Release|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}.Release|Any CPU.Build.0 = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Linux|Any CPU.ActiveCfg = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Linux|Any CPU.Build.0 = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}.Release|Any CPU.Build.0 = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Linux|Any CPU.ActiveCfg = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Linux|Any CPU.Build.0 = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8ED51D01-158C-4B29-824A-35B9B861E45A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EA13C704-483F-4CE4-A3FB-8F79295F1071} + EndGlobalSection +EndGlobal diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 677025aab..da890bbba 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -26,6 +26,7 @@ namespace Microsoft.PowerShell { class ExitException : Exception { } + class LineAcceptedException : Exception { } public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods { @@ -44,7 +45,10 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods private bool _delayedOneTimeInitCompleted; // This is used by AIShell to check if PSReadLine is initialized and ready to render. + #pragma warning disable CS0414 private bool _readLineReady; + #pragma warning restore CS0414 + private bool _lineAcceptedExceptionThrown; private IPSConsoleReadLineMockableMethods _mockableMethods; private IConsole _console; @@ -175,9 +179,18 @@ internal static PSKeyInfo ReadKey() // By waiting for a key on a different thread, our pipeline execution thread // (the thread ReadLine is called from) avoid being blocked in code that can't // be unblocked and instead blocks on events we control. - - // First, set an event so the thread to read a key actually attempts to read a key. - _singleton._readKeyWaitHandle.Set(); + if (_singleton._lineAcceptedExceptionThrown) + { + // If we threw a 'LineAcceptedException', it means that "AcceptLine" was called within an 'OnIdle' handler the last time + // this method was called, and thus we didn't wait for '_keyReadWaitHandle' to be signalled by the 'readkey thread'. + // In this case, we don't want to signal '_readKeyWaitHandle' again as the 'readkey thread' already got a chance to run. + _singleton._lineAcceptedExceptionThrown = false; + } + else + { + // Set an event so the 'readkey thread' actually attempts to read a key. + _singleton._readKeyWaitHandle.Set(); + } int handleId; System.Management.Automation.PowerShell ps = null; @@ -277,6 +290,16 @@ internal static PSKeyInfo ReadKey() _singleton.Render(); } } + + if (_singleton._inputAccepted && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // 'AcceptLine' was called by an 'OnIdle' handler. + // In this case, we only want to break out of the loop and accept the current input on Windows, because + // accepting input without a keystroke would leave the 'readkey thread' blocked on the 'ReadKey()' call, + // and that will make all subsequent writes to console blocked on Linux and macOS until a key is pressed. + _singleton._lineAcceptedExceptionThrown = true; + throw new LineAcceptedException(); + } } } } @@ -531,8 +554,16 @@ private string InputLoop() // window resizing cannot and shouldn't happen within the processing of a given keybinding. _handlePotentialResizing = true; - var key = ReadKey(); - ProcessOneKey(key, _dispatchTable, ignoreIfNoAction: false, arg: null); + try + { + var key = ReadKey(); + ProcessOneKey(key, _dispatchTable, ignoreIfNoAction: false, arg: null); + } + catch (LineAcceptedException) + { + Debug.Assert(_inputAccepted, "LineAcceptedException should only be thrown when input was accepted within an 'OnIdle' handler."); + } + if (_inputAccepted) { _acceptedCommandLine = _buffer.ToString(); @@ -1035,16 +1066,28 @@ public static void DigitArgument(ConsoleKeyInfo? key = null, object arg = null) public static void InvokePrompt(ConsoleKeyInfo? key = null, object arg = null) { var console = _singleton._console; - console.CursorVisible = false; if (arg is int newY) { + if (newY < 0 || newY >= console.BufferHeight) + { + throw new ArgumentOutOfRangeException(nameof(arg)); + } + + console.CursorVisible = false; console.SetCursorPosition(0, newY); } else { newY = _singleton._initialY - _singleton._options.ExtraPromptLineCount; + // Silently return if user has implicitly requested an impossible prompt invocation. + if (newY < 0) + { + return; + } + + console.CursorVisible = false; console.SetCursorPosition(0, newY); // We need to rewrite the prompt, so blank out everything from a previous prompt invocation diff --git a/PSReadLine/SamplePSReadLineProfile.ps1 b/PSReadLine/SamplePSReadLineProfile.ps1 index 0956dc2f3..6da7e8617 100644 --- a/PSReadLine/SamplePSReadLineProfile.ps1 +++ b/PSReadLine/SamplePSReadLineProfile.ps1 @@ -522,7 +522,7 @@ Set-PSReadLineKeyHandler -Key F1 ` # # Ctrl+Shift+j then type a key to mark the current directory. -# Ctrj+j then the same key will change back to that directory without +# Ctrl+j then the same key will change back to that directory without # needing to type cd and won't change the command line. # diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index ad248096b..d09e2abc9 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -537,6 +537,9 @@ private void TestMustDing(string expectedResult, object[] items) private string _emptyLine; private TestConsole _console; private MockedMethods _mockedMethods; + private bool _oneTimeInitCompleted; + private object _psrlInstance; + private FieldInfo _psrlConsole, _psrlMockableMethods; private static string MakeCombinedColor(ConsoleColor fg, ConsoleColor bg) => VTColorUtils.AsEscapeSequence(fg) + VTColorUtils.AsEscapeSequence(bg, isBackground: true); @@ -553,14 +556,17 @@ private void TestSetup(TestConsole console, KeyMode keyMode, params KeyHandler[] _console = console ?? new TestConsole(_); _mockedMethods = new MockedMethods(); - var instance = (PSConsoleReadLine)typeof(PSConsoleReadLine) - .GetField("_singleton", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); - typeof(PSConsoleReadLine) - .GetField("_mockableMethods", BindingFlags.Instance | BindingFlags.NonPublic) - .SetValue(instance, _mockedMethods); - typeof(PSConsoleReadLine) - .GetField("_console", BindingFlags.Instance | BindingFlags.NonPublic) - .SetValue(instance, _console); + + if (_psrlInstance is null) + { + Type psrlType = typeof(PSConsoleReadLine); + _psrlInstance = psrlType.GetField("_singleton", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); + _psrlConsole = psrlType.GetField("_console", BindingFlags.Instance | BindingFlags.NonPublic); + _psrlMockableMethods = psrlType.GetField("_mockableMethods", BindingFlags.Instance | BindingFlags.NonPublic); + } + + _psrlConsole.SetValue(_psrlInstance, _console); + _psrlMockableMethods.SetValue(_psrlInstance, _mockedMethods); _emptyLine ??= new string(' ', _console.BufferWidth); @@ -626,6 +632,13 @@ private void TestSetup(TestConsole console, KeyMode keyMode, params KeyHandler[] } var colorOptions = new SetPSReadLineOption {Colors = colors}; PSConsoleReadLine.SetOptions(colorOptions); + + if (!_oneTimeInitCompleted) + { + typeof(PSConsoleReadLine).GetMethod("Initialize", BindingFlags.Instance | BindingFlags.NonPublic) + .Invoke(_psrlInstance, new object[] { /* Runspace */ null, /* EngineIntrinsics */ null, }); + _oneTimeInitCompleted = true; + } } }