Skip to content

Commit 1ea00df

Browse files
authored
Handle buffer changes made by an event handler (#4442)
1 parent 9e946af commit 1ea00df

File tree

2 files changed

+39
-12
lines changed

2 files changed

+39
-12
lines changed

PSReadLine/PublicAPI.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ public static void Insert(char c)
9292
/// <param name="s">String to insert</param>
9393
public static void Insert(string s)
9494
{
95+
s = s.Replace("\r\n", "\n");
9596
_singleton.SaveEditItem(EditItemInsertString.Create(s, _singleton._current));
9697

9798
// Use Append if possible because Insert at end makes StringBuilder quite slow.

PSReadLine/ReadLine.cs

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)