Skip to content

Commit 175096b

Browse files
committed
Implement SafeRender() for use under a screen reader
1 parent d5b5999 commit 175096b

File tree

4 files changed

+148
-49
lines changed

4 files changed

+148
-49
lines changed

PSReadLine/BasicEditing.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public static void CancelLine(ConsoleKeyInfo? key = null, object arg = null)
8686
_singleton._current = _singleton._buffer.Length;
8787

8888
using var _ = _singleton._prediction.DisableScoped();
89-
_singleton.ForceRender();
89+
_singleton.Render(force: true);
9090

9191
_singleton._console.Write("\x1b[91m^C\x1b[0m");
9292

@@ -335,7 +335,7 @@ private bool AcceptLineImpl(bool validate)
335335

336336
if (renderNeeded)
337337
{
338-
ForceRender();
338+
Render(force: true);
339339
}
340340

341341
// Only run validation if we haven't before. If we have and status line shows an error,

PSReadLine/Options.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,12 @@ private void SetOptionsInternal(SetPSReadLineOption options)
189189
{
190190
Options.ScreenReader = options.ScreenReader;
191191

192-
// Disable prediction for better accessibility
193192
if (Options.ScreenReader)
194193
{
194+
// Disable prediction for better accessibility.
195195
Options.PredictionSource = PredictionSource.None;
196+
// Disable continuation prompt as multi-line is not available.
197+
Options.ContinuationPrompt = "";
196198
}
197199
}
198200
}

PSReadLine/Render.Helper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ internal static int LengthInBufferCells(char c)
9696
if (c < 256)
9797
{
9898
// We render ^C for Ctrl+C, so return 2 for control characters
99+
// TODO: Do we care about this under a screen reader?
99100
return Char.IsControl(c) ? 2 : 1;
100101
}
101102

PSReadLine/Render.cs

Lines changed: 142 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,11 @@ private void RenderWithPredictionQueryPaused()
218218
Render();
219219
}
220220

221-
private void Render()
221+
private void Render(bool force = false)
222222
{
223223
// If there are a bunch of keys queued up, skip rendering if we've rendered very recently.
224224
long elapsedMs = _lastRenderTime.ElapsedMilliseconds;
225-
if (_queuedKeys.Count > 10 && elapsedMs < 50)
225+
if (!force && _queuedKeys.Count > 10 && elapsedMs < 50)
226226
{
227227
// We won't render, but most likely the tokens will be different, so make
228228
// sure we don't use old tokens, also allow garbage to get collected.
@@ -242,12 +242,142 @@ private void Render()
242242
// See the following 2 GitHub issues for more context:
243243
// - https://github.com/PowerShell/PSReadLine/issues/3879#issuecomment-2573996070
244244
// - https://github.com/PowerShell/PowerShell/issues/24696
245-
if (elapsedMs < 50)
245+
if (!force && elapsedMs < 50)
246246
{
247247
_handlePotentialResizing = false;
248248
}
249249

250-
ForceRender();
250+
// Use simplified rendering for screen readers
251+
if (Options.ScreenReader)
252+
{
253+
SafeRender();
254+
}
255+
else
256+
{
257+
ForceRender();
258+
}
259+
}
260+
261+
private Point CalculateNextInputPoint(int _current)
262+
{
263+
var point = ConvertOffsetToPoint(_current);
264+
265+
if (point.Y == _console.BufferHeight)
266+
{
267+
// The cursor top exceeds the buffer height, so we need to
268+
// scroll up the buffer by 1 line.
269+
_console.Write("\n");
270+
271+
// Adjust the initial cursor position and the to-be-set cursor position
272+
// after scrolling up the buffer.
273+
_initialY -= 1;
274+
point.Y -= 1;
275+
}
276+
else if (point.Y < 0)
277+
{
278+
// This could happen in at least 3 cases:
279+
//
280+
// 1. when you are adding characters to the first line in the buffer (top = 0) to make the logical line
281+
// wrap to one extra physical line. This would cause the buffer to scroll up and push the line being
282+
// edited up-off the buffer.
283+
// 2. when you are deleting characters (Backspace) from the first line in the buffer without changing the
284+
// number of physical lines (either editing the same logical line or causing the current logical line
285+
// to merge in the previous but still span to the current physical line). The cursor is supposed to
286+
// appear in the previous line (which is off the buffer).
287+
// 3. Both 'bck-i-search' and 'fwd-i-search' may find a history command with multi-line text, and the
288+
// matching string in the text, where the cursor is supposed to be moved to, will be scrolled up-off
289+
// the buffer after rendering.
290+
//
291+
// In these case, we move the cursor to the left-most position of the first line, where it's closest to
292+
// the real position it should be in the ideal world.
293+
294+
// First update '_current' to the index of the first character that appears on the line 0,
295+
// then we call 'ConvertOffsetToPoint' again to get the right cursor position to use.
296+
point.X = point.Y = 0;
297+
_current = ConvertLineAndColumnToOffset(point);
298+
point = ConvertOffsetToPoint(_current);
299+
}
300+
301+
return point;
302+
}
303+
304+
private void SafeRender()
305+
{
306+
static int FindCommonPrefixLength(string leftStr, string rightStr)
307+
{
308+
if (string.IsNullOrEmpty(leftStr) || string.IsNullOrEmpty(rightStr))
309+
{
310+
return 0;
311+
}
312+
313+
int i = 0;
314+
int minLength = Math.Min(leftStr.Length, rightStr.Length);
315+
316+
while (i < minLength && leftStr[i] == rightStr[i])
317+
{
318+
i++;
319+
}
320+
321+
return i;
322+
}
323+
324+
// For screen readers, we don't need complex tokenization or coloring.
325+
// Just compare the raw buffer content and output differences.
326+
// (Though we do have to adjust the cursor aftewards.)
327+
string currentBuffer = _buffer.ToString(); // Just like GenerateRender().
328+
string previousBuffer = _previousRender.lines[0].Line;
329+
330+
// In case the buffer was resized.
331+
RecomputeInitialCoords(isTextBufferUnchanged: false);
332+
333+
// Make cursor invisible while we're rendering.
334+
_console.CursorVisible = false;
335+
336+
// Calculate what to render and where to start the rendering.
337+
int commonPrefixLength = FindCommonPrefixLength(previousBuffer, currentBuffer);
338+
339+
if (commonPrefixLength > 0 && commonPrefixLength == previousBuffer.Length)
340+
{
341+
// Previous buffer is a complete prefix of current buffer.
342+
// Just append the new data.
343+
var appendedData = currentBuffer.Substring(commonPrefixLength);
344+
_console.Write(appendedData);
345+
}
346+
else if (commonPrefixLength > 0)
347+
{
348+
// Buffers share a common prefix but previous buffer has additional content.
349+
// Move cursor to where the difference starts, clear forward, and write the data.
350+
var diffPoint = ConvertOffsetToPoint(commonPrefixLength);
351+
_console.SetCursorPosition(diffPoint.X, diffPoint.Y);
352+
var changedData = currentBuffer.Substring(commonPrefixLength);
353+
_console.Write("\x1b[0J");
354+
_console.Write(changedData);
355+
}
356+
else
357+
{
358+
// No common prefix, rewrite entire buffer.
359+
_console.SetCursorPosition(_initialX, _initialY);
360+
_console.Write("\x1b[0J");
361+
_console.Write(currentBuffer);
362+
}
363+
364+
// Preserve the current render data.
365+
var renderData = new RenderData
366+
{
367+
lines = new RenderedLineData[] { new(currentBuffer, isFirstLogicalLine: true) }
368+
};
369+
_previousRender = renderData;
370+
371+
// Calculate the coord to place the cursor for the next input.
372+
var point = CalculateNextInputPoint(_current);
373+
_console.SetCursorPosition(point.X, point.Y);
374+
_console.CursorVisible = true;
375+
376+
renderData.UpdateConsoleInfo(_console.BufferWidth, _console.BufferHeight, point.X, point.Y);
377+
renderData.initialY = _initialY;
378+
379+
_lastRenderTime.Restart();
380+
_waitingToRender = false;
251381
}
252382

253383
private void ForceRender()
@@ -261,7 +391,7 @@ private void ForceRender()
261391
// and minimize writing more than necessary on the next render.)
262392

263393
var renderLines = new RenderedLineData[logicalLineCount];
264-
var renderData = new RenderData {lines = renderLines};
394+
var renderData = new RenderData { lines = renderLines };
265395
for (var i = 0; i < logicalLineCount; i++)
266396
{
267397
var line = _consoleBufferLines[i].ToString();
@@ -909,44 +1039,7 @@ void UpdateColorsIfNecessary(string newColor)
9091039
}
9101040

9111041
// Calculate the coord to place the cursor for the next input.
912-
var point = ConvertOffsetToPoint(_current);
913-
914-
if (point.Y == bufferHeight)
915-
{
916-
// The cursor top exceeds the buffer height, so we need to
917-
// scroll up the buffer by 1 line.
918-
_console.Write("\n");
919-
920-
// Adjust the initial cursor position and the to-be-set cursor position
921-
// after scrolling up the buffer.
922-
_initialY -= 1;
923-
point.Y -= 1;
924-
}
925-
else if (point.Y < 0)
926-
{
927-
// This could happen in at least 3 cases:
928-
//
929-
// 1. when you are adding characters to the first line in the buffer (top = 0) to make the logical line
930-
// wrap to one extra physical line. This would cause the buffer to scroll up and push the line being
931-
// edited up-off the buffer.
932-
// 2. when you are deleting characters (Backspace) from the first line in the buffer without changing the
933-
// number of physical lines (either editing the same logical line or causing the current logical line
934-
// to merge in the previous but still span to the current physical line). The cursor is supposed to
935-
// appear in the previous line (which is off the buffer).
936-
// 3. Both 'bck-i-search' and 'fwd-i-search' may find a history command with multi-line text, and the
937-
// matching string in the text, where the cursor is supposed to be moved to, will be scrolled up-off
938-
// the buffer after rendering.
939-
//
940-
// In these case, we move the cursor to the left-most position of the first line, where it's closest to
941-
// the real position it should be in the ideal world.
942-
943-
// First update '_current' to the index of the first character that appears on the line 0,
944-
// then we call 'ConvertOffsetToPoint' again to get the right cursor position to use.
945-
point.X = point.Y = 0;
946-
_current = ConvertLineAndColumnToOffset(point);
947-
point = ConvertOffsetToPoint(_current);
948-
}
949-
1042+
var point = CalculateNextInputPoint(_current);
9501043
_console.SetCursorPosition(point.X, point.Y);
9511044
_console.CursorVisible = true;
9521045

@@ -1178,8 +1271,11 @@ private void MoveCursor(int newCursor)
11781271
// to issuing a newline to scroll the buffer.
11791272
_console.SetCursorPosition(point.X, point.Y);
11801273

1181-
// Scroll up the buffer by 1 line.
1182-
_console.Write("\n");
1274+
// Scroll up the buffer by 1 line, except under a screen reader which naturally wraps.
1275+
if (!Options.ScreenReader)
1276+
{
1277+
_console.Write("\n");
1278+
}
11831279
}
11841280
else
11851281
{
@@ -1827,6 +1923,6 @@ public static void ScrollDisplayToCursor(ConsoleKeyInfo? key = null, object arg
18271923
newTop = (console.BufferHeight - console.WindowHeight);
18281924
}
18291925
console.SetWindowPosition(0, newTop);
1830-
}
1926+
}
18311927
}
18321928
}

0 commit comments

Comments
 (0)