@@ -218,36 +218,197 @@ private void RenderWithPredictionQueryPaused()
218218 Render ( ) ;
219219 }
220220
221- private void Render ( )
221+ private void Render ( bool force = false )
222222 {
223- // If there are a bunch of keys queued up, skip rendering if we've rendered very recently.
224- long elapsedMs = _lastRenderTime . ElapsedMilliseconds ;
225- if ( _queuedKeys . Count > 10 && elapsedMs < 50 )
226- {
227- // We won't render, but most likely the tokens will be different, so make
228- // sure we don't use old tokens, also allow garbage to get collected.
229- _tokens = null ;
230- _ast = null ;
231- _parseErrors = null ;
232- _waitingToRender = true ;
233- return ;
223+ if ( ! force )
224+ {
225+ // If there are a bunch of keys queued up, skip rendering if we've rendered very recently.
226+ long elapsedMs = _lastRenderTime . ElapsedMilliseconds ;
227+ if ( _queuedKeys . Count > 10 && elapsedMs < 50 )
228+ {
229+ // We won't render, but most likely the tokens will be different, so make
230+ // sure we don't use old tokens, also allow garbage to get collected.
231+ _tokens = null ;
232+ _ast = null ;
233+ _parseErrors = null ;
234+ _waitingToRender = true ;
235+ return ;
236+ }
237+
238+ // If we've rendered very recently, skip the terminal window resizing check as it's unlikely
239+ // to happen in such a short time interval.
240+ // We try to avoid unnecessary resizing check because it requires getting the cursor position
241+ // which would force a network round trip in an environment where front-end xtermjs talking to
242+ // a server-side PTY via websocket. Without querying for cursor position, content written on
243+ // the server side could be buffered, which is much more performant.
244+ // See the following 2 GitHub issues for more context:
245+ // - https://github.com/PowerShell/PSReadLine/issues/3879#issuecomment-2573996070
246+ // - https://github.com/PowerShell/PowerShell/issues/24696
247+ if ( elapsedMs < 50 )
248+ {
249+ _handlePotentialResizing = false ;
250+ }
251+ }
252+
253+ // Use simplified rendering for screen readers
254+ if ( Options . ScreenReaderModeEnabled )
255+ {
256+ RenderForScreenReader ( ) ;
257+ }
258+ else
259+ {
260+ ForceRender ( ) ;
261+ }
262+ }
263+
264+ private void RenderForScreenReader ( )
265+ {
266+ int bufferWidth = _console . BufferWidth ;
267+ int bufferHeight = _console . BufferHeight ;
268+
269+ static int FindCommonPrefixLength ( string leftStr , string rightStr )
270+ {
271+ if ( string . IsNullOrEmpty ( leftStr ) || string . IsNullOrEmpty ( rightStr ) )
272+ {
273+ return 0 ;
274+ }
275+
276+ int i = 0 ;
277+ int minLength = Math . Min ( leftStr . Length , rightStr . Length ) ;
278+
279+ while ( i < minLength && leftStr [ i ] == rightStr [ i ] )
280+ {
281+ i ++ ;
282+ }
283+
284+ return i ;
285+ }
286+
287+ // For screen readers, we are just comparing the previous and current buffer text
288+ // (without colors) and only writing the differences.
289+ //
290+ // Note that we don't call QueryForSuggestion() which is the only
291+ // entry into the prediction logic, so while it could be enabled, it
292+ // won't do anything in this rendering implementation.
293+ string parsedInput = ParseInput ( ) ;
294+ StringBuilder buffer = new ( parsedInput ) ;
295+
296+ // Really simple handling of a status line: append it!
297+ if ( ! string . IsNullOrEmpty ( _statusLinePrompt ) )
298+ {
299+ buffer . Append ( "\n " ) ;
300+ buffer . Append ( _statusLinePrompt ) ;
301+ buffer . Append ( _statusBuffer ) ;
302+ }
303+
304+ string currentBuffer = buffer . ToString ( ) ;
305+ string previousBuffer = _previousRender . lines [ 0 ] . Line ;
306+
307+ // In case the buffer was resized.
308+ RecomputeInitialCoords ( isTextBufferUnchanged : false ) ;
309+
310+ // Make cursor invisible while we're rendering.
311+ _console . CursorVisible = false ;
312+
313+ if ( currentBuffer == previousBuffer )
314+ {
315+ // No-op, such as when selecting text or otherwise re-entering.
316+ }
317+ else if ( previousBuffer . Length == 0 )
318+ {
319+ // Previous buffer was empty so we just render the current buffer.
320+ _console . SetCursorPosition ( _initialX , _initialY ) ;
321+ _console . Write ( currentBuffer ) ;
234322 }
323+ else
324+ {
325+ // Calculate what to render and where to start the rendering.
326+ int commonPrefixLength = FindCommonPrefixLength ( previousBuffer , currentBuffer ) ;
327+
328+ // If we're scrolling through history we always want to re-render.
329+ // Writing only the diff in this scenario is a weird UX.
330+ if ( commonPrefixLength > 0 && _anyHistoryCommandCount == 0 )
331+ {
332+ // We need to differentially render, possibly with a partial rewrite.
333+ if ( commonPrefixLength != previousBuffer . Length )
334+ {
335+ // The buffers share a common prefix but the previous buffer has additional content.
336+ // Move cursor to where the difference starts and clear so we can rewrite.
337+ var diffPoint = ConvertOffsetToPoint ( commonPrefixLength , buffer ) ;
338+ _console . SetCursorPosition ( diffPoint . X , diffPoint . Y ) ;
339+ _console . Write ( "\x1b [0J" ) ;
340+ } // Otherwise the previous buffer is a complete prefix and we just write.
341+
342+ // TODO: There is a rare edge case where the common prefix can be incorrectly
343+ // calculated because the incoming replacement text matches the text at the current
344+ // cursor position. Unfortunately we don't have a solution yet. For example:
345+ //
346+ // 1. Previous line is "abcdef" and cursor is at (before) the letter "d"
347+ // 2. Paste "defghi" so currentBuffer is "abcdefghidef"
348+ // 3. The diff is "ghidef" and because commonPrefixLength == previousBuffer.Length
349+ // 4. The terminal will incorrectly display "abcghidef" instead of "abcdefghidef"
350+
351+ // Finally, write the diff.
352+ var diffData = currentBuffer . Substring ( commonPrefixLength ) ;
353+ _console . Write ( diffData ) ;
354+ }
355+ else
356+ {
357+ // The buffers are completely different so we need to rewrite from the start.
358+ _console . SetCursorPosition ( _initialX , _initialY ) ;
359+ _console . Write ( "\x1b [0J" ) ;
360+ _console . Write ( currentBuffer ) ;
361+ }
362+ }
363+
364+ // If we had to wrap to render everything, update _initialY
365+ var endPoint = ConvertOffsetToPoint ( currentBuffer . Length , buffer ) ;
366+ if ( endPoint . Y >= bufferHeight )
367+ {
368+ // We had to scroll to render everything, update _initialY.
369+ int offset = 1 ; // Base case to handle zero-indexing.
370+ if ( endPoint . X == 0 )
371+ {
372+ // The line hasn't actually wrapped yet because we have exactly filled the line.
373+ offset -= 1 ;
374+ }
375+ int scrolledLines = endPoint . Y - bufferHeight + offset ;
376+ _initialY -= scrolledLines ;
377+ }
378+
379+ // Calculate the coord to place the cursor for the next input.
380+ var point = ConvertOffsetToPoint ( _current , buffer ) ;
235381
236- // If we've rendered very recently, skip the terminal window resizing check as it's unlikely
237- // to happen in such a short time interval.
238- // We try to avoid unnecessary resizing check because it requires getting the cursor position
239- // which would force a network round trip in an environment where front-end xtermjs talking to
240- // a server-side PTY via websocket. Without querying for cursor position, content written on
241- // the server side could be buffered, which is much more performant.
242- // See the following 2 GitHub issues for more context:
243- // - https://github.com/PowerShell/PSReadLine/issues/3879#issuecomment-2573996070
244- // - https://github.com/PowerShell/PowerShell/issues/24696
245- if ( elapsedMs < 50 )
382+ if ( point . Y == bufferHeight )
246383 {
247- _handlePotentialResizing = false ;
384+ // The cursor top exceeds the buffer height and it hasn't already wrapped,
385+ // (because we have exactly filled the line) so we need to scroll up the buffer by 1 line.
386+ if ( point . X == 0 && ! currentBuffer . EndsWith ( "\n " ) )
387+ {
388+ _console . Write ( "\n " ) ;
389+ }
390+
391+ // Adjust the initial cursor position and the to-be-set cursor position
392+ // after scrolling up the buffer.
393+ _initialY -= 1 ;
394+ point . Y -= 1 ;
248395 }
249396
250- ForceRender ( ) ;
397+ _console . SetCursorPosition ( point . X , point . Y ) ;
398+ _console . CursorVisible = true ;
399+
400+ // Preserve the current render data.
401+ var renderData = new RenderData
402+ {
403+ lines = new RenderedLineData [ ] { new ( currentBuffer , isFirstLogicalLine : true ) } ,
404+ errorPrompt = ( _parseErrors != null && _parseErrors . Length > 0 ) // Not yet used.
405+ } ;
406+ _previousRender = renderData ;
407+ _previousRender . UpdateConsoleInfo ( bufferWidth , bufferHeight , point . X , point . Y ) ;
408+ _previousRender . initialY = _initialY ;
409+
410+ _lastRenderTime . Restart ( ) ;
411+ _waitingToRender = false ;
251412 }
252413
253414 private void ForceRender ( )
@@ -261,7 +422,7 @@ private void ForceRender()
261422 // and minimize writing more than necessary on the next render.)
262423
263424 var renderLines = new RenderedLineData [ logicalLineCount ] ;
264- var renderData = new RenderData { lines = renderLines } ;
425+ var renderData = new RenderData { lines = renderLines } ;
265426 for ( var i = 0 ; i < logicalLineCount ; i ++ )
266427 {
267428 var line = _consoleBufferLines [ i ] . ToString ( ) ;
@@ -872,9 +1033,6 @@ void UpdateColorsIfNecessary(string newColor)
8721033 WriteBlankLines ( lineCount ) ;
8731034 }
8741035
875- // Preserve the current render data.
876- _previousRender = renderData ;
877-
8781036 // If we counted pseudo physical lines, deduct them to get the real physical line counts
8791037 // before updating '_initialY'.
8801038 physicalLine -= pseudoPhysicalLineOffset ;
@@ -950,6 +1108,8 @@ void UpdateColorsIfNecessary(string newColor)
9501108 _console . SetCursorPosition ( point . X , point . Y ) ;
9511109 _console . CursorVisible = true ;
9521110
1111+ // Preserve the current render data.
1112+ _previousRender = renderData ;
9531113 _previousRender . UpdateConsoleInfo ( bufferWidth , bufferHeight , point . X , point . Y ) ;
9541114 _previousRender . initialY = _initialY ;
9551115
@@ -1201,17 +1361,23 @@ internal Point EndOfBufferPosition()
12011361 return ConvertOffsetToPoint ( _buffer . Length ) ;
12021362 }
12031363
1204- internal Point ConvertOffsetToPoint ( int offset )
1364+ internal Point ConvertOffsetToPoint ( int offset , StringBuilder buffer = null )
12051365 {
1366+ // This lets us re-use the logic in the screen reader rendering implementation
1367+ // where the status line is added to the buffer without modifying the local state.
1368+ buffer ??= _buffer ;
1369+
12061370 int x = _initialX ;
12071371 int y = _initialY ;
12081372
12091373 int bufferWidth = _console . BufferWidth ;
1210- var continuationPromptLength = LengthInBufferCells ( Options . ContinuationPrompt ) ;
1374+ var continuationPromptLength = Options . ScreenReaderModeEnabled
1375+ ? 0
1376+ : LengthInBufferCells ( Options . ContinuationPrompt ) ;
12111377
12121378 for ( int i = 0 ; i < offset ; i ++ )
12131379 {
1214- char c = _buffer [ i ] ;
1380+ char c = buffer [ i ] ;
12151381 if ( c == '\n ' )
12161382 {
12171383 y += 1 ;
@@ -1229,7 +1395,7 @@ internal Point ConvertOffsetToPoint(int offset)
12291395
12301396 // If cursor is at column 0 and the next character is newline, let the next loop
12311397 // iteration increment y.
1232- if ( x != 0 || ! ( i + 1 < offset && _buffer [ i + 1 ] == '\n ' ) )
1398+ if ( x != 0 || ! ( i + 1 < offset && buffer [ i + 1 ] == '\n ' ) )
12331399 {
12341400 y += 1 ;
12351401 }
@@ -1238,9 +1404,9 @@ internal Point ConvertOffsetToPoint(int offset)
12381404 }
12391405
12401406 // If next character actually exists, and isn't newline, check if wider than the space left on the current line.
1241- if ( _buffer . Length > offset && _buffer [ offset ] != '\n ' )
1407+ if ( buffer . Length > offset && buffer [ offset ] != '\n ' )
12421408 {
1243- int size = LengthInBufferCells ( _buffer [ offset ] ) ;
1409+ int size = LengthInBufferCells ( buffer [ offset ] ) ;
12441410 if ( x + size > bufferWidth )
12451411 {
12461412 // Character was wider than remaining space, so character, and cursor, appear on next line.
@@ -1259,7 +1425,10 @@ private int ConvertLineAndColumnToOffset(Point point)
12591425 int y = _initialY ;
12601426
12611427 int bufferWidth = _console . BufferWidth ;
1262- var continuationPromptLength = LengthInBufferCells ( Options . ContinuationPrompt ) ;
1428+ var continuationPromptLength = Options . ScreenReaderModeEnabled
1429+ ? 0
1430+ : LengthInBufferCells ( Options . ContinuationPrompt ) ;
1431+
12631432 for ( offset = 0 ; offset < _buffer . Length ; offset ++ )
12641433 {
12651434 // If we are on the correct line, return when we find
0 commit comments