Skip to content

Handle buffer changes made by a handler of the OnIdle event #4442

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PSReadLine/PublicAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public static void Insert(char c)
/// <param name="s">String to insert</param>
public static void Insert(string s)
{
s = s.Replace("\r\n", "\n");
_singleton.SaveEditItem(EditItemInsertString.Create(s, _singleton._current));

// Use Append if possible because Insert at end makes StringBuilder quite slow.
Expand Down
50 changes: 38 additions & 12 deletions PSReadLine/ReadLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ internal static PSKeyInfo ReadKey()
// If we timed out, check for event subscribers (which is just
// a hint that there might be an event waiting to be processed.)
var eventSubscribers = _singleton._engineIntrinsics?.Events.Subscribers;
int bufferLen = _singleton._buffer.Length;
if (eventSubscribers?.Count > 0)
{
bool runPipelineForEventProcessing = false;
Expand All @@ -211,16 +212,20 @@ internal static PSKeyInfo ReadKey()
if (string.Equals(sub.SourceIdentifier, PSEngineEvent.OnIdle, StringComparison.OrdinalIgnoreCase))
{
// If the buffer is not empty, let's not consider we are idle because the user is in the middle of typing something.
if (_singleton._buffer.Length > 0)
if (bufferLen > 0)
{
continue;
}

// There is an OnIdle event subscriber and we are idle because we timed out and the buffer is empty.
// Normally PowerShell generates this event, but PowerShell assumes the engine is not idle because
// it called PSConsoleHostReadLine which isn't returning. So we generate the event instead.
// There is an 'OnIdle' event subscriber and we are idle because we timed out and the buffer is empty.
// Normally PowerShell generates this event, but now PowerShell assumes the engine is not idle because
// it called 'PSConsoleHostReadLine' which isn't returning. So we generate the event instead.
runPipelineForEventProcessing = true;
_singleton._engineIntrinsics.Events.GenerateEvent(PSEngineEvent.OnIdle, null, null, null);
_singleton._engineIntrinsics.Events.GenerateEvent(
PSEngineEvent.OnIdle,
sender: null,
args: null,
extraData: null);

// Break out so we don't genreate more than one 'OnIdle' event for a timeout.
break;
Expand All @@ -239,15 +244,36 @@ internal static PSKeyInfo ReadKey()
ps.AddScript("[System.Diagnostics.DebuggerHidden()]param() 0", useLocalScope: true);
}

// To detect output during possible event processing, see if the cursor moved
// and rerender if so.
var console = _singleton._console;
var y = console.CursorTop;
// To detect output during possible event processing, see if the cursor moved and rerender if so.
int cursorTop = _singleton._console.CursorTop;

// Start the pipeline to process events.
ps.Invoke();
if (y != console.CursorTop)

// Check if any event handler writes console output to the best of our effort, and adjust the initial coordinates in that case.
//
// I say "to the best of our effort" because the delegate handler for an event will mostly run on a background thread, and thus
// there is no guarantee about when the delegate would finish. So in an extreme case, there could be race conditions in console
// read/write: we are reading 'CursorTop' while the delegate is writing console output on a different thread.
// There is no much we can do about that extreme case. However, our focus here is the 'OnIdle' event, and its handler is usually
// a script block, which will run within the 'ps.Invoke()' call above.
//
// We detect new console output by checking if cursor top changed, but handle a very special case: an event handler changed our
// buffer, by calling 'Insert' for example.
// I know only checking on buffer length change doesn't cover the case where buffer changed but the length is the same. However,
// we mainly want to cover buffer changes made by an 'OnIdle' event handler, and we trigger 'OnIdle' event only if the buffer is
// empty. So, this check is efficient and good enough for that main scenario.
// When our buffer was changed by an event handler, we assume that was all the event handler did and there was no direct console
// output. So, we adjust the initial coordinates only if cursor top changed but there was no buffer change.
int newCursorTop = _singleton._console.CursorTop;
int newBufferLen = _singleton._buffer.Length;
if (cursorTop != newCursorTop && bufferLen == newBufferLen)
{
_singleton._initialY = console.CursorTop;
_singleton.Render();
_singleton._initialY = newCursorTop;
if (bufferLen > 0)
{
_singleton.Render();
}
}
}
}
Expand Down