diff --git a/shell/AIShell.Integration/AIShell.psm1 b/shell/AIShell.Integration/AIShell.psm1 index bac0bc66..aeb02f7e 100644 --- a/shell/AIShell.Integration/AIShell.psm1 +++ b/shell/AIShell.Integration/AIShell.psm1 @@ -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. diff --git a/shell/AIShell.Integration/Channel.cs b/shell/AIShell.Integration/Channel.cs index 47069c5a..6dfe2b68 100644 --- a/shell/AIShell.Integration/Channel.cs +++ b/shell/AIShell.Integration/Channel.cs @@ -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; @@ -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()"); @@ -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); @@ -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]); } + + /// + /// 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'. + /// + 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 PredictionCandidates);