@@ -203,6 +203,7 @@ internal static PSKeyInfo ReadKey()
203203 // If we timed out, check for event subscribers (which is just
204204 // a hint that there might be an event waiting to be processed.)
205205 var eventSubscribers = _singleton . _engineIntrinsics ? . Events . Subscribers ;
206+ int bufferLen = _singleton . _buffer . Length ;
206207 if ( eventSubscribers ? . Count > 0 )
207208 {
208209 bool runPipelineForEventProcessing = false ;
@@ -211,16 +212,20 @@ internal static PSKeyInfo ReadKey()
211212 if ( string . Equals ( sub . SourceIdentifier , PSEngineEvent . OnIdle , StringComparison . OrdinalIgnoreCase ) )
212213 {
213214 // If the buffer is not empty, let's not consider we are idle because the user is in the middle of typing something.
214- if ( _singleton . _buffer . Length > 0 )
215+ if ( bufferLen > 0 )
215216 {
216217 continue ;
217218 }
218219
219- // There is an OnIdle event subscriber and we are idle because we timed out and the buffer is empty.
220- // Normally PowerShell generates this event, but PowerShell assumes the engine is not idle because
221- // it called PSConsoleHostReadLine which isn't returning. So we generate the event instead.
220+ // There is an ' OnIdle' event subscriber and we are idle because we timed out and the buffer is empty.
221+ // Normally PowerShell generates this event, but now PowerShell assumes the engine is not idle because
222+ // it called ' PSConsoleHostReadLine' which isn't returning. So we generate the event instead.
222223 runPipelineForEventProcessing = true ;
223- _singleton . _engineIntrinsics . Events . GenerateEvent ( PSEngineEvent . OnIdle , null , null , null ) ;
224+ _singleton . _engineIntrinsics . Events . GenerateEvent (
225+ PSEngineEvent . OnIdle ,
226+ sender : null ,
227+ args : null ,
228+ extraData : null ) ;
224229
225230 // Break out so we don't genreate more than one 'OnIdle' event for a timeout.
226231 break ;
@@ -239,15 +244,36 @@ internal static PSKeyInfo ReadKey()
239244 ps . AddScript ( "[System.Diagnostics.DebuggerHidden()]param() 0" , useLocalScope : true ) ;
240245 }
241246
242- // To detect output during possible event processing, see if the cursor moved
243- // and rerender if so.
244- var console = _singleton . _console ;
245- var y = console . CursorTop ;
247+ // To detect output during possible event processing, see if the cursor moved and rerender if so.
248+ int cursorTop = _singleton . _console . CursorTop ;
249+
250+ // Start the pipeline to process events.
246251 ps . Invoke ( ) ;
247- if ( y != console . CursorTop )
252+
253+ // Check if any event handler writes console output to the best of our effort, and adjust the initial coordinates in that case.
254+ //
255+ // I say "to the best of our effort" because the delegate handler for an event will mostly run on a background thread, and thus
256+ // there is no guarantee about when the delegate would finish. So in an extreme case, there could be race conditions in console
257+ // read/write: we are reading 'CursorTop' while the delegate is writing console output on a different thread.
258+ // There is no much we can do about that extreme case. However, our focus here is the 'OnIdle' event, and its handler is usually
259+ // a script block, which will run within the 'ps.Invoke()' call above.
260+ //
261+ // We detect new console output by checking if cursor top changed, but handle a very special case: an event handler changed our
262+ // buffer, by calling 'Insert' for example.
263+ // I know only checking on buffer length change doesn't cover the case where buffer changed but the length is the same. However,
264+ // we mainly want to cover buffer changes made by an 'OnIdle' event handler, and we trigger 'OnIdle' event only if the buffer is
265+ // empty. So, this check is efficient and good enough for that main scenario.
266+ // When our buffer was changed by an event handler, we assume that was all the event handler did and there was no direct console
267+ // output. So, we adjust the initial coordinates only if cursor top changed but there was no buffer change.
268+ int newCursorTop = _singleton . _console . CursorTop ;
269+ int newBufferLen = _singleton . _buffer . Length ;
270+ if ( cursorTop != newCursorTop && bufferLen == newBufferLen )
248271 {
249- _singleton . _initialY = console . CursorTop ;
250- _singleton . Render ( ) ;
272+ _singleton . _initialY = newCursorTop ;
273+ if ( bufferLen > 0 )
274+ {
275+ _singleton . Render ( ) ;
276+ }
251277 }
252278 }
253279 }
0 commit comments