Skip to content

Fix code posting on macOS #366

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
Apr 16, 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
4 changes: 2 additions & 2 deletions shell/AIShell.Integration/AIShell.psm1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
$module = Get-Module -Name PSReadLine
if ($null -eq $module -or $module.Version -lt [version]"2.4.1") {
throw "The PSReadLine v2.4.1-beta1 or higher is required for the AIShell module to work properly."
if ($null -eq $module -or $module.Version -lt [version]"2.4.2") {
throw "The PSReadLine v2.4.2-beta2 or higher is required for the AIShell module to work properly."
}

## Create the channel singleton when loading the module.
Expand Down
62 changes: 55 additions & 7 deletions shell/AIShell.Integration/Channel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class Channel : IDisposable
private readonly Type _psrlType;
private readonly Runspace _runspace;
private readonly MethodInfo _psrlInsert, _psrlRevertLine, _psrlAcceptLine;
private readonly FieldInfo _psrlHandleResizing, _psrlReadLineReady;
private readonly object _psrlSingleton;
private readonly ManualResetEvent _connSetupWaitHandler;
private readonly Predictor _predictor;
private readonly ScriptBlock _onIdleAction;
Expand All @@ -40,10 +42,17 @@ private Channel(Runspace runspace, Type psConsoleReadLineType)
.Append(Path.GetFileNameWithoutExtension(Environment.ProcessPath))
.ToString();

BindingFlags bindingFlags = BindingFlags.Static | BindingFlags.Public;
_psrlInsert = _psrlType.GetMethod("Insert", bindingFlags, [typeof(string)]);
_psrlRevertLine = _psrlType.GetMethod("RevertLine", bindingFlags);
_psrlAcceptLine = _psrlType.GetMethod("AcceptLine", bindingFlags);
BindingFlags methodFlags = BindingFlags.Static | BindingFlags.Public;
_psrlInsert = _psrlType.GetMethod("Insert", methodFlags, [typeof(string)]);
_psrlRevertLine = _psrlType.GetMethod("RevertLine", methodFlags);
_psrlAcceptLine = _psrlType.GetMethod("AcceptLine", methodFlags);

FieldInfo singletonInfo = _psrlType.GetField("_singleton", BindingFlags.Static | BindingFlags.NonPublic);
_psrlSingleton = singletonInfo.GetValue(null);

BindingFlags fieldFlags = BindingFlags.Instance | BindingFlags.NonPublic;
_psrlReadLineReady = _psrlType.GetField("_readLineReady", fieldFlags);
_psrlHandleResizing = _psrlType.GetField("_handlePotentialResizing", fieldFlags);

_predictor = new Predictor();
_onIdleAction = ScriptBlock.Create("[AIShell.Integration.Channel]::Singleton.OnIdleHandler()");
Expand Down Expand Up @@ -217,10 +226,9 @@ private void OnPostCode(PostCodeMessage postCodeMessage)
codeToInsert = sb.ToString();
}

// When PSReadLine is actively running, 'TreatControlCAsInput' would be set to 'true' because
// it handles 'Ctrl+c' as regular input.
// When PSReadLine is actively running, its '_readLineReady' field should be set to 'true'.
// When the value is 'false', it means PowerShell is still busy running scripts or commands.
if (Console.TreatControlCAsInput)
if (_psrlReadLineReady.GetValue(_psrlSingleton) is true)
{
PSRLRevertLine();
PSRLInsert(codeToInsert);
Expand Down Expand Up @@ -268,18 +276,58 @@ private void OnAskConnection(ShellClientPipe clientPipe, Exception exception)

private void PSRLInsert(string text)
{
using var _ = new NoWindowResizingCheck();
_psrlInsert.Invoke(null, [text]);
}

private void PSRLRevertLine()
{
using var _ = new NoWindowResizingCheck();
_psrlRevertLine.Invoke(null, [null, null]);
}

private void PSRLAcceptLine()
{
using var _ = new NoWindowResizingCheck();
_psrlAcceptLine.Invoke(null, [null, null]);
}

/// <summary>
/// We assume the terminal window will not resize during the code-post operation and hence disable the window resizing check on macOS.
/// This is to avoid reading console cursor positions while PSReadLine is already blocked on 'Console.ReadKey', because on Unix system,
/// when we are already blocked on key input, reading cursor position on another thread will be blocked too until a key is pressed.
///
/// We do need window resizing check on Windows due to how 'Start-AIShell' works differently:
/// - On Windows, 'Start-AIShell' returns way BEFORE the current tab gets splitted for the sidecar pane, and PowerShell has already
/// called into PSReadLine when the splitting actually happens. So, it's literally a window resizing for PSReadLine at that point
/// and hence we need the window resizing check to correct the initial coordinates ('_initialX' and '_initialY').
/// - On macOS, however, 'Start-AIShell' returns AFTER the current tab gets splitted for the sidecar pane. So, window resizing will
/// be done before PowerShell calls into PSReadLine and hence there is no need for window resizing check on macOS.
/// Also, On Windows we can read cursor position without blocking even if another thread is blocked on calling 'ReadKey'.
/// </summary>
private class NoWindowResizingCheck : IDisposable
{
private readonly object _originalValue;

internal NoWindowResizingCheck()
{
if (OperatingSystem.IsMacOS())
{
Channel channel = Singleton;
_originalValue = channel._psrlHandleResizing.GetValue(channel._psrlSingleton);
channel._psrlHandleResizing.SetValue(channel._psrlSingleton, false);
}
}

public void Dispose()
{
if (OperatingSystem.IsMacOS())
{
Channel channel = Singleton;
channel._psrlHandleResizing.SetValue(channel._psrlSingleton, _originalValue);
}
}
}
}

internal record CodePostData(string CodeToInsert, List<PredictionCandidate> PredictionCandidates);