@@ -203,6 +203,7 @@ internal static PSKeyInfo ReadKey()
203
203
// If we timed out, check for event subscribers (which is just
204
204
// a hint that there might be an event waiting to be processed.)
205
205
var eventSubscribers = _singleton . _engineIntrinsics ? . Events . Subscribers ;
206
+ int bufferLen = _singleton . _buffer . Length ;
206
207
if ( eventSubscribers ? . Count > 0 )
207
208
{
208
209
bool runPipelineForEventProcessing = false ;
@@ -211,16 +212,20 @@ internal static PSKeyInfo ReadKey()
211
212
if ( string . Equals ( sub . SourceIdentifier , PSEngineEvent . OnIdle , StringComparison . OrdinalIgnoreCase ) )
212
213
{
213
214
// 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 )
215
216
{
216
217
continue ;
217
218
}
218
219
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.
222
223
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 ) ;
224
229
225
230
// Break out so we don't genreate more than one 'OnIdle' event for a timeout.
226
231
break ;
@@ -239,15 +244,36 @@ internal static PSKeyInfo ReadKey()
239
244
ps . AddScript ( "[System.Diagnostics.DebuggerHidden()]param() 0" , useLocalScope : true ) ;
240
245
}
241
246
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.
246
251
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 )
248
271
{
249
- _singleton . _initialY = console . CursorTop ;
250
- _singleton . Render ( ) ;
272
+ _singleton . _initialY = newCursorTop ;
273
+ if ( bufferLen > 0 )
274
+ {
275
+ _singleton . Render ( ) ;
276
+ }
251
277
}
252
278
}
253
279
}
0 commit comments