From 06c70c84d887b49a1e96f90cbfe0c9e46a8f667b Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:40:23 -0700 Subject: [PATCH 1/7] Add `ScreenReader` support CLI option That should default to enabled when one is detected on startup, but also allows the support to be forcibly enabled. --- PSReadLine/Accessibility.cs | 3 ++- PSReadLine/Cmdlets.cs | 17 +++++++++++++++++ PSReadLine/Options.cs | 4 ++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/PSReadLine/Accessibility.cs b/PSReadLine/Accessibility.cs index 4938da233..c1dfe897a 100644 --- a/PSReadLine/Accessibility.cs +++ b/PSReadLine/Accessibility.cs @@ -14,8 +14,9 @@ internal static bool IsScreenReaderActive() if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + // NOTE: This API can detect if a third-party screen reader is active, such as NVDA, but not the in-box Windows Narrator. PlatformWindows.SystemParametersInfo(PlatformWindows.SPI_GETSCREENREADER, 0, ref returnValue, 0); - } + } // TODO: Support other platforms per https://code.visualstudio.com/docs/configure/accessibility/accessibility return returnValue; } diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index 7fecf4b4e..af4d633f5 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -15,6 +15,7 @@ using System.Runtime.InteropServices; using System.Threading; using Microsoft.PowerShell.PSReadLine; +using Microsoft.PowerShell.Internal; using AllowNull = System.Management.Automation.AllowNullAttribute; namespace Microsoft.PowerShell @@ -163,6 +164,11 @@ public class PSConsoleReadLineOptions /// public const int DefaultAnsiEscapeTimeout = 100; + /// + /// If screen reader support is enabled, which enables safe rendering using ANSI control codes. + /// + public static readonly bool DefaultScreenReader = Accessibility.IsScreenReaderActive(); + static PSConsoleReadLineOptions() { // For inline-view suggestion text, we use the new FG color 'dim white italic' when possible, because it provides @@ -224,6 +230,7 @@ public PSConsoleReadLineOptions(string hostName, bool usingLegacyConsole) : PredictionSource.HistoryAndPlugin; PredictionViewStyle = DefaultPredictionViewStyle; MaximumHistoryCount = 0; + ScreenReader = DefaultScreenReader; var historyFileName = hostName + "_history.txt"; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -533,6 +540,8 @@ public object ListPredictionTooltipColor public bool TerminateOrphanedConsoleApps { get; set; } + public bool ScreenReader { get; set; } + internal string _defaultTokenColor; internal string _commentColor; internal string _keywordColor; @@ -847,6 +856,14 @@ public SwitchParameter TerminateOrphanedConsoleApps } internal SwitchParameter? _terminateOrphanedConsoleApps; + [Parameter] + public SwitchParameter ScreenReader + { + get => _screenReader.GetValueOrDefault(); + set => _screenReader = value; + } + internal SwitchParameter? _screenReader; + [ExcludeFromCodeCoverage] protected override void EndProcessing() { diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index 7485154b4..b8b3efad5 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -185,6 +185,10 @@ private void SetOptionsInternal(SetPSReadLineOption options) nameof(Options.TerminateOrphanedConsoleApps))); } } + if (options._screenReader.HasValue) + { + Options.ScreenReader = options.ScreenReader; + } } private void SetKeyHandlerInternal(string[] keys, Action handler, string briefDescription, string longDescription, ScriptBlock scriptBlock) From 4baae08fb7e6b5ae371a0fbfcdb1fb050ce0a2e2 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 2 Jul 2025 18:57:12 -0700 Subject: [PATCH 2/7] Improve screen reader detection on Windows by checking modules Borrows the "better" algorithm from Electron, with attribution. --- PSReadLine/Accessibility.cs | 40 +++++++++++++++++++++++++++++------ PSReadLine/PlatformWindows.cs | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 6 deletions(-) diff --git a/PSReadLine/Accessibility.cs b/PSReadLine/Accessibility.cs index c1dfe897a..fa000555b 100644 --- a/PSReadLine/Accessibility.cs +++ b/PSReadLine/Accessibility.cs @@ -10,15 +10,43 @@ internal class Accessibility { internal static bool IsScreenReaderActive() { - bool returnValue = false; + // TODO: Support other platforms per https://code.visualstudio.com/docs/configure/accessibility/accessibility + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return false; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + // The supposedly official way to check for a screen reader on + // Windows is SystemParametersInfo(SPI_GETSCREENREADER, ...) but it + // doesn't detect the in-box Windows Narrator and is otherwise known + // to be problematic. + // + // The following is adapted from the Electron project, under the MIT License. + // Hence this is also how VS Code detects screenreaders. + // See: https://github.com/electron/electron/pull/39988 + + // Check for Windows Narrator using the NarratorRunning mutex + if (PlatformWindows.IsMutexPresent("NarratorRunning")) + return true; + + // Check for various screen reader libraries + string[] screenReaderLibraries = { + // NVDA + "nvdaHelperRemote.dll", + // JAWS + "jhook.dll", + // Window-Eyes + "gwhk64.dll", + "gwmhook.dll", + // ZoomText + "AiSquared.Infuser.HookLib.dll" + }; + + foreach (string library in screenReaderLibraries) { - // NOTE: This API can detect if a third-party screen reader is active, such as NVDA, but not the in-box Windows Narrator. - PlatformWindows.SystemParametersInfo(PlatformWindows.SPI_GETSCREENREADER, 0, ref returnValue, 0); - } // TODO: Support other platforms per https://code.visualstudio.com/docs/configure/accessibility/accessibility + if (PlatformWindows.IsLibraryLoaded(library)) + return true; + } - return returnValue; + return false; } } } diff --git a/PSReadLine/PlatformWindows.cs b/PSReadLine/PlatformWindows.cs index c7e0313b9..a28e33368 100644 --- a/PSReadLine/PlatformWindows.cs +++ b/PSReadLine/PlatformWindows.cs @@ -79,6 +79,43 @@ IntPtr templateFileWin32Handle [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern IntPtr GetStdHandle(uint handleId); + internal const int ERROR_ALREADY_EXISTS = 0xB7; + + internal static bool IsMutexPresent(string name) + { + try + { + using var mutex = new System.Threading.Mutex(false, name); + return Marshal.GetLastWin32Error() == ERROR_ALREADY_EXISTS; + } + catch + { + return false; + } + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool GetModuleHandleEx(uint dwFlags, string lpModuleName, out IntPtr phModule); + + internal static bool IsLibraryLoaded(string name) + { + // Prevents the reference count for the module being incremented. + const uint GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 0x00000002; + try + { + bool retValue = GetModuleHandleEx( + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + name, + out IntPtr phModule); + return phModule != IntPtr.Zero; + } + catch + { + return false; + } + } + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool SetConsoleCtrlHandler(BreakHandler handlerRoutine, bool add); From a6dc61674553cda5dd3fb12b6111534a45b5571b Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:09:10 -0700 Subject: [PATCH 3/7] Check for VoiceOver screen reader on macOS Spawns a quick `defaults` process since in .NET using the macOS events is difficult, but this is quick and easy. --- PSReadLine/Accessibility.cs | 50 +++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/PSReadLine/Accessibility.cs b/PSReadLine/Accessibility.cs index fa000555b..e9a54a4ed 100644 --- a/PSReadLine/Accessibility.cs +++ b/PSReadLine/Accessibility.cs @@ -2,6 +2,7 @@ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ +using System.Diagnostics; using System.Runtime.InteropServices; namespace Microsoft.PowerShell.Internal @@ -10,10 +11,22 @@ internal class Accessibility { internal static bool IsScreenReaderActive() { - // TODO: Support other platforms per https://code.visualstudio.com/docs/configure/accessibility/accessibility - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return IsAnyWindowsScreenReaderEnabled(); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return IsVoiceOverEnabled(); + } + + // TODO: Support Linux per https://code.visualstudio.com/docs/configure/accessibility/accessibility return false; + } + private static bool IsAnyWindowsScreenReaderEnabled() + { // The supposedly official way to check for a screen reader on // Windows is SystemParametersInfo(SPI_GETSCREENREADER, ...) but it // doesn't detect the in-box Windows Narrator and is otherwise known @@ -48,5 +61,38 @@ internal static bool IsScreenReaderActive() return false; } + + private static bool IsVoiceOverEnabled() + { + try + { + // Use the 'defaults' command to check if VoiceOver is enabled + // This checks the com.apple.universalaccess preference for voiceOverOnOffKey + ProcessStartInfo startInfo = new() + { + FileName = "defaults", + Arguments = "read com.apple.universalaccess voiceOverOnOffKey", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using Process process = Process.Start(startInfo); + process.WaitForExit(250); + if (process.HasExited && process.ExitCode == 0) + { + string output = process.StandardOutput.ReadToEnd().Trim(); + // VoiceOver is enabled if the value is 1 + return output == "1"; + } + } + catch + { + // If we can't determine the status, assume VoiceOver is not enabled + } + + return false; + } } } From 207e358c29ecd4e93ea8b82e44441eef54aba91f Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:04:29 -0700 Subject: [PATCH 4/7] Revert to system parameter screen reader check on Windows That algorithm doesn't work for a non-windowed app like PowerShell. --- PSReadLine/Accessibility.cs | 40 +++++++++++++++-------------------- PSReadLine/PlatformWindows.cs | 22 ------------------- 2 files changed, 17 insertions(+), 45 deletions(-) diff --git a/PSReadLine/Accessibility.cs b/PSReadLine/Accessibility.cs index e9a54a4ed..aae6c0f52 100644 --- a/PSReadLine/Accessibility.cs +++ b/PSReadLine/Accessibility.cs @@ -22,7 +22,7 @@ internal static bool IsScreenReaderActive() } // TODO: Support Linux per https://code.visualstudio.com/docs/configure/accessibility/accessibility - return false; + return false; } private static bool IsAnyWindowsScreenReaderEnabled() @@ -32,31 +32,25 @@ private static bool IsAnyWindowsScreenReaderEnabled() // doesn't detect the in-box Windows Narrator and is otherwise known // to be problematic. // - // The following is adapted from the Electron project, under the MIT License. - // Hence this is also how VS Code detects screenreaders. - // See: https://github.com/electron/electron/pull/39988 - - // Check for Windows Narrator using the NarratorRunning mutex - if (PlatformWindows.IsMutexPresent("NarratorRunning")) + // Unfortunately, the alternative method used by Electron and + // Chromium, where the relevant screen reader libraries (modules) + // are checked for does not work in the context of PowerShell + // because it relies on those applications injecting themselves into + // the app. Which they do not because PowerShell is not a windowed + // app, so we're stuck using the known-to-be-buggy way. + bool spiScreenReader = false; + PlatformWindows.SystemParametersInfo(PlatformWindows.SPI_GETSCREENREADER, 0, ref spiScreenReader, 0); + if (spiScreenReader) + { return true; + } - // Check for various screen reader libraries - string[] screenReaderLibraries = { - // NVDA - "nvdaHelperRemote.dll", - // JAWS - "jhook.dll", - // Window-Eyes - "gwhk64.dll", - "gwmhook.dll", - // ZoomText - "AiSquared.Infuser.HookLib.dll" - }; - - foreach (string library in screenReaderLibraries) + // At least we can correctly check for Windows Narrator using the + // NarratorRunning mutex. Windows Narrator is mostly not broken with + // PSReadLine, not in the way that NVDA and VoiceOver are. + if (PlatformWindows.IsMutexPresent("NarratorRunning")) { - if (PlatformWindows.IsLibraryLoaded(library)) - return true; + return true; } return false; diff --git a/PSReadLine/PlatformWindows.cs b/PSReadLine/PlatformWindows.cs index a28e33368..32cf653fb 100644 --- a/PSReadLine/PlatformWindows.cs +++ b/PSReadLine/PlatformWindows.cs @@ -94,28 +94,6 @@ internal static bool IsMutexPresent(string name) } } - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool GetModuleHandleEx(uint dwFlags, string lpModuleName, out IntPtr phModule); - - internal static bool IsLibraryLoaded(string name) - { - // Prevents the reference count for the module being incremented. - const uint GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT = 0x00000002; - try - { - bool retValue = GetModuleHandleEx( - GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - name, - out IntPtr phModule); - return phModule != IntPtr.Zero; - } - catch - { - return false; - } - } - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool SetConsoleCtrlHandler(BreakHandler handlerRoutine, bool add); From c38dbaeeaa2ef034f9c7044a2827cf42ebe652b0 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Thu, 17 Jul 2025 18:31:23 -0700 Subject: [PATCH 5/7] Disable predictions for screen readers Because they'll be rendered and it's useless noise. --- PSReadLine/Options.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index b8b3efad5..19ba6b3bd 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -188,6 +188,12 @@ private void SetOptionsInternal(SetPSReadLineOption options) if (options._screenReader.HasValue) { Options.ScreenReader = options.ScreenReader; + + // Disable prediction for better accessibility + if (Options.ScreenReader) + { + Options.PredictionSource = PredictionSource.None; + } } } From 2c3b9cf96100ffe7d62b2db16cdee57055b8c5a7 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Wed, 30 Jul 2025 22:05:40 -0700 Subject: [PATCH 6/7] Implement `SafeRender()` for use under a screen reader --- PSReadLine/BasicEditing.cs | 4 +- PSReadLine/Options.cs | 4 +- PSReadLine/Render.Helper.cs | 1 + PSReadLine/Render.cs | 165 +++++++++++++++++++++++++++++++----- 4 files changed, 148 insertions(+), 26 deletions(-) diff --git a/PSReadLine/BasicEditing.cs b/PSReadLine/BasicEditing.cs index 33445ed36..74c76ac00 100644 --- a/PSReadLine/BasicEditing.cs +++ b/PSReadLine/BasicEditing.cs @@ -86,7 +86,7 @@ public static void CancelLine(ConsoleKeyInfo? key = null, object arg = null) _singleton._current = _singleton._buffer.Length; using var _ = _singleton._prediction.DisableScoped(); - _singleton.ForceRender(); + _singleton.Render(force: true); _singleton._console.Write("\x1b[91m^C\x1b[0m"); @@ -335,7 +335,7 @@ private bool AcceptLineImpl(bool validate) if (renderNeeded) { - ForceRender(); + Render(force: true); } // Only run validation if we haven't before. If we have and status line shows an error, diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index 19ba6b3bd..0416dba3e 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -189,10 +189,12 @@ private void SetOptionsInternal(SetPSReadLineOption options) { Options.ScreenReader = options.ScreenReader; - // Disable prediction for better accessibility if (Options.ScreenReader) { + // Disable prediction for better accessibility. Options.PredictionSource = PredictionSource.None; + // Disable continuation prompt as multi-line is not available. + Options.ContinuationPrompt = ""; } } } diff --git a/PSReadLine/Render.Helper.cs b/PSReadLine/Render.Helper.cs index 8fc56bea1..584ce7956 100644 --- a/PSReadLine/Render.Helper.cs +++ b/PSReadLine/Render.Helper.cs @@ -96,6 +96,7 @@ internal static int LengthInBufferCells(char c) if (c < 256) { // We render ^C for Ctrl+C, so return 2 for control characters + // TODO: Do we care about this under a screen reader? return Char.IsControl(c) ? 2 : 1; } diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index fe688481a..d1af0c388 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -218,36 +218,155 @@ private void RenderWithPredictionQueryPaused() Render(); } - private void Render() + private void Render(bool force = false) { // If there are a bunch of keys queued up, skip rendering if we've rendered very recently. long elapsedMs = _lastRenderTime.ElapsedMilliseconds; - if (_queuedKeys.Count > 10 && elapsedMs < 50) - { - // We won't render, but most likely the tokens will be different, so make - // sure we don't use old tokens, also allow garbage to get collected. - _tokens = null; - _ast = null; - _parseErrors = null; - _waitingToRender = true; - return; + if (!force) + { + if (_queuedKeys.Count > 10 && elapsedMs < 50) + { + // We won't render, but most likely the tokens will be different, so make + // sure we don't use old tokens, also allow garbage to get collected. + _tokens = null; + _ast = null; + _parseErrors = null; + _waitingToRender = true; + return; + } + + // If we've rendered very recently, skip the terminal window resizing check as it's unlikely + // to happen in such a short time interval. + // We try to avoid unnecessary resizing check because it requires getting the cursor position + // which would force a network round trip in an environment where front-end xtermjs talking to + // a server-side PTY via websocket. Without querying for cursor position, content written on + // the server side could be buffered, which is much more performant. + // See the following 2 GitHub issues for more context: + // - https://github.com/PowerShell/PSReadLine/issues/3879#issuecomment-2573996070 + // - https://github.com/PowerShell/PowerShell/issues/24696 + if (elapsedMs < 50) + { + _handlePotentialResizing = false; + } } - // If we've rendered very recently, skip the terminal window resizing check as it's unlikely - // to happen in such a short time interval. - // We try to avoid unnecessary resizing check because it requires getting the cursor position - // which would force a network round trip in an environment where front-end xtermjs talking to - // a server-side PTY via websocket. Without querying for cursor position, content written on - // the server side could be buffered, which is much more performant. - // See the following 2 GitHub issues for more context: - // - https://github.com/PowerShell/PSReadLine/issues/3879#issuecomment-2573996070 - // - https://github.com/PowerShell/PowerShell/issues/24696 - if (elapsedMs < 50) + // Use simplified rendering for screen readers + if (Options.ScreenReader) { - _handlePotentialResizing = false; + SafeRender(); } + else + { + ForceRender(); + } + } + + private void SafeRender() + { + int bufferWidth = _console.BufferWidth; + int bufferHeight = _console.BufferHeight; + + static int FindCommonPrefixLength(string leftStr, string rightStr) + { + if (string.IsNullOrEmpty(leftStr) || string.IsNullOrEmpty(rightStr)) + { + return 0; + } - ForceRender(); + int i = 0; + int minLength = Math.Min(leftStr.Length, rightStr.Length); + + while (i < minLength && leftStr[i] == rightStr[i]) + { + i++; + } + + return i; + } + + // For screen readers, we are just comparing the previous and current buffer text + // (without colors) and only writing the differences. + string currentBuffer = ParseInput(); + string previousBuffer = _previousRender.lines[0].Line; + + // In case the buffer was resized. + RecomputeInitialCoords(isTextBufferUnchanged: false); + + // Make cursor invisible while we're rendering. + _console.CursorVisible = false; + + // Calculate what to render and where to start the rendering. + // TODO: Short circuit optimization when currentBuffer == previousBuffer. + int commonPrefixLength = FindCommonPrefixLength(previousBuffer, currentBuffer); + + if (commonPrefixLength > 0 && commonPrefixLength == previousBuffer.Length) + { + // Previous buffer is a complete prefix of current buffer. + // Just append the new data. + var appendedData = currentBuffer.Substring(commonPrefixLength); + _console.Write(appendedData); + } + else if (commonPrefixLength > 0) + { + // Buffers share a common prefix but previous buffer has additional content. + // Move cursor to where the difference starts, clear forward, and write the data. + var diffPoint = ConvertOffsetToPoint(commonPrefixLength); + _console.SetCursorPosition(diffPoint.X, diffPoint.Y); + var changedData = currentBuffer.Substring(commonPrefixLength); + _console.Write("\x1b[0J"); + _console.Write(changedData); + } + else + { + // No common prefix, rewrite entire buffer. + _console.SetCursorPosition(_initialX, _initialY); + _console.Write("\x1b[0J"); + _console.Write(currentBuffer); + } + + // If we had to wrap to render everything, update _initialY + var endPoint = ConvertOffsetToPoint(currentBuffer.Length); + int physicalLine = endPoint.Y - _initialY; + if (_initialY + physicalLine > bufferHeight) + { + // We had to scroll to render everything, update _initialY. + _initialY = bufferHeight - physicalLine; + } + + // Preserve the current render data. + var renderData = new RenderData + { + lines = new RenderedLineData[] { new(currentBuffer, isFirstLogicalLine: true) }, + errorPrompt = (_parseErrors != null && _parseErrors.Length > 0) // Not yet used. + }; + _previousRender = renderData; + + // Calculate the coord to place the cursor for the next input. + var point = ConvertOffsetToPoint(_current); + + if (point.Y == bufferHeight) + { + // The cursor top exceeds the buffer height and it hasn't already wrapped, + // so we need to scroll up the buffer by 1 line. + if (point.X == 0) + { + _console.Write("\n"); + } + + // Adjust the initial cursor position and the to-be-set cursor position + // after scrolling up the buffer. + _initialY -= 1; + point.Y -= 1; + } + + _console.SetCursorPosition(point.X, point.Y); + _console.CursorVisible = true; + + _previousRender.UpdateConsoleInfo(bufferWidth, bufferHeight, point.X, point.Y); + _previousRender.initialY = _initialY; + + _lastRenderTime.Restart(); + _waitingToRender = false; } private void ForceRender() @@ -261,7 +380,7 @@ private void ForceRender() // and minimize writing more than necessary on the next render.) var renderLines = new RenderedLineData[logicalLineCount]; - var renderData = new RenderData {lines = renderLines}; + var renderData = new RenderData { lines = renderLines }; for (var i = 0; i < logicalLineCount; i++) { var line = _consoleBufferLines[i].ToString(); From 14fc559ff9be75031f0bc190b70ef3a7c6ee41c8 Mon Sep 17 00:00:00 2001 From: Andy Jordan <2226434+andyleejordan@users.noreply.github.com> Date: Tue, 5 Aug 2025 16:45:22 -0700 Subject: [PATCH 7/7] Correctly handle edge case with exactly filled line --- PSReadLine/Render.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index d1af0c388..f13d15b96 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -326,11 +326,18 @@ static int FindCommonPrefixLength(string leftStr, string rightStr) // If we had to wrap to render everything, update _initialY var endPoint = ConvertOffsetToPoint(currentBuffer.Length); - int physicalLine = endPoint.Y - _initialY; - if (_initialY + physicalLine > bufferHeight) + if (endPoint.Y >= bufferHeight) { + // We had to scroll to render everything, update _initialY. - _initialY = bufferHeight - physicalLine; + int offset = 1; // Base case to handle zero-indexing. + if (endPoint.X == 0) + { + // The line hasn't actually wrapped yet because we have exactly filled the line. + offset -= 1; + } + int scrolledLines = endPoint.Y - bufferHeight + offset; + _initialY -= scrolledLines; } // Preserve the current render data. @@ -347,8 +354,8 @@ static int FindCommonPrefixLength(string leftStr, string rightStr) if (point.Y == bufferHeight) { // The cursor top exceeds the buffer height and it hasn't already wrapped, - // so we need to scroll up the buffer by 1 line. - if (point.X == 0) + // (because we have exactly filled the line) so we need to scroll up the buffer by 1 line. + if (point.X == 0 && !currentBuffer.EndsWith("\n")) { _console.Write("\n"); }