diff --git a/MockPSConsole/MockPSConsole.csproj b/MockPSConsole/MockPSConsole.csproj index 164e083ae..6f57f1342 100644 --- a/MockPSConsole/MockPSConsole.csproj +++ b/MockPSConsole/MockPSConsole.csproj @@ -18,7 +18,7 @@ - + diff --git a/MockPSConsole/Program.cs b/MockPSConsole/Program.cs index b26684b5b..f7c68f0a5 100644 --- a/MockPSConsole/Program.cs +++ b/MockPSConsole/Program.cs @@ -99,7 +99,7 @@ static void Main() ps.Commands.Clear(); Console.Write(string.Join("", ps.AddCommand("prompt").Invoke())); - var line = PSConsoleReadLine.ReadLine(rs, executionContext); + var line = PSConsoleReadLine.ReadLine(rs, executionContext, lastRunStatus: null); Console.WriteLine(line); line = line.Trim(); if (line.Equals("exit")) diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 151d4161c..6e90804a8 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -22,7 +22,7 @@ - + diff --git a/PSReadLine/PSReadLine.psm1 b/PSReadLine/PSReadLine.psm1 index 6b33bd5a1..74c271419 100644 --- a/PSReadLine/PSReadLine.psm1 +++ b/PSReadLine/PSReadLine.psm1 @@ -1,5 +1,8 @@ function PSConsoleHostReadLine { + ## Get the execution status of the last accepted user input. + ## This needs to be done as the first thing because any script run will flush $?. + $lastRunStatus = $? Microsoft.PowerShell.Core\Set-StrictMode -Off - [Microsoft.PowerShell.PSConsoleReadLine]::ReadLine($host.Runspace, $ExecutionContext) + [Microsoft.PowerShell.PSConsoleReadLine]::ReadLine($host.Runspace, $ExecutionContext, $lastRunStatus) } diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index ad207e95b..ed9a4bf93 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; -using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.Prediction; using Microsoft.PowerShell.Internal; namespace Microsoft.PowerShell @@ -33,12 +33,12 @@ protected PredictionViewBase(PSConsoleReadLine singleton) /// /// Gets whether to use plugin as a source. /// - protected bool UsePlugin => (_singleton._options.PredictionSource & PredictionSource.Plugin) != 0; + internal bool UsePlugin => (_singleton._options.PredictionSource & PredictionSource.Plugin) != 0; /// /// Gets whether to use history as a source. /// - protected bool UseHistory => (_singleton._options.PredictionSource & PredictionSource.History) != 0; + internal bool UseHistory => (_singleton._options.PredictionSource & PredictionSource.History) != 0; /// /// Gets whether an update to the view is pending. @@ -80,17 +80,6 @@ internal virtual void Reset() _predictionTask = null; } - /// - /// Get called when a command line is accepted. - /// - internal void OnCommandLineAccepted(string commandLine) - { - if (UsePlugin && !string.IsNullOrWhiteSpace(commandLine)) - { - _singleton._mockableMethods.OnCommandLineAccepted(_singleton._recentHistory.ToArray()); - } - } - /// /// Currently we only select single-line history that is prefixed with the user input, /// but it can be improved to not strictly use the user input as a prefix, but a hint @@ -193,7 +182,7 @@ protected List GetHistorySuggestions(string input, int count) /// protected void PredictInput() { - _predictionTask = _singleton._mockableMethods.PredictInput(_singleton._ast, _singleton._tokens); + _predictionTask = _singleton._mockableMethods.PredictInputAsync(_singleton._ast, _singleton._tokens); } /// diff --git a/PSReadLine/Prediction.cs b/PSReadLine/Prediction.cs index 597cb02c5..42edb5f84 100644 --- a/PSReadLine/Prediction.cs +++ b/PSReadLine/Prediction.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using System.Management.Automation; using System.Management.Automation.Language; -using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.Prediction; using System.Diagnostics.CodeAnalysis; using Microsoft.PowerShell.Internal; using Microsoft.PowerShell.PSReadLine; @@ -17,34 +17,50 @@ namespace Microsoft.PowerShell public partial class PSConsoleReadLine { private const string PSReadLine = "PSReadLine"; + private static PredictionClient s_predictionClient = new(PSReadLine, PredictionClientKind.Terminal); // Stub helper methods so prediction can be mocked [ExcludeFromCodeCoverage] - Task> IPSConsoleReadLineMockableMethods.PredictInput(Ast ast, Token[] tokens) + Task> IPSConsoleReadLineMockableMethods.PredictInputAsync(Ast ast, Token[] tokens) { - return CommandPrediction.PredictInput(PSReadLine, ast, tokens); + return CommandPrediction.PredictInputAsync(s_predictionClient, ast, tokens); } [ExcludeFromCodeCoverage] void IPSConsoleReadLineMockableMethods.OnSuggestionDisplayed(Guid predictorId, uint session, int countOrIndex) { - CommandPrediction.OnSuggestionDisplayed(PSReadLine, predictorId, session, countOrIndex); + CommandPrediction.OnSuggestionDisplayed(s_predictionClient, predictorId, session, countOrIndex); } [ExcludeFromCodeCoverage] void IPSConsoleReadLineMockableMethods.OnSuggestionAccepted(Guid predictorId, uint session, string suggestionText) { - CommandPrediction.OnSuggestionAccepted(PSReadLine, predictorId, session, suggestionText); + CommandPrediction.OnSuggestionAccepted(s_predictionClient, predictorId, session, suggestionText); } [ExcludeFromCodeCoverage] void IPSConsoleReadLineMockableMethods.OnCommandLineAccepted(IReadOnlyList history) { - CommandPrediction.OnCommandLineAccepted(PSReadLine, history); + CommandPrediction.OnCommandLineAccepted(s_predictionClient, history); + } + + [ExcludeFromCodeCoverage] + void IPSConsoleReadLineMockableMethods.OnCommandLineExecuted(string commandLine, bool success) + { + CommandPrediction.OnCommandLineExecuted(s_predictionClient, commandLine, success); } private readonly Prediction _prediction; + /// + /// Report the execution result (success or failure) of the last accepted command line. + /// + /// Whether the execution was successful. + private void ReportExecutionStatus(bool success) + { + _prediction.OnCommandLineExecuted(_acceptedCommandLine, success); + } + /// /// Accept the suggestion text if there is one. /// @@ -381,6 +397,28 @@ internal bool RevertSuggestion() return retValue; } + + /// + /// Get called when a command line is accepted. + /// + internal void OnCommandLineAccepted(string commandLine) + { + if (ActiveView.UsePlugin && !string.IsNullOrWhiteSpace(commandLine)) + { + _singleton._mockableMethods.OnCommandLineAccepted(_singleton._recentHistory.ToArray()); + } + } + + /// + /// Get called when the last accepted command line finished execution. + /// + internal void OnCommandLineExecuted(string commandLine, bool success) + { + if (ActiveView.UsePlugin && !string.IsNullOrWhiteSpace(commandLine)) + { + _singleton._mockableMethods.OnCommandLineExecuted(commandLine, success); + } + } } } } diff --git a/PSReadLine/PublicAPI.cs b/PSReadLine/PublicAPI.cs index e661b29a9..3e9696686 100644 --- a/PSReadLine/PublicAPI.cs +++ b/PSReadLine/PublicAPI.cs @@ -9,7 +9,7 @@ using System.Management.Automation; using System.Management.Automation.Language; using System.Management.Automation.Runspaces; -using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.Prediction; using System.Text; using System.Threading.Tasks; using Microsoft.PowerShell.PSReadLine; @@ -26,8 +26,9 @@ public interface IPSConsoleReadLineMockableMethods void Ding(); CommandCompletion CompleteInput(string input, int cursorIndex, Hashtable options, System.Management.Automation.PowerShell powershell); bool RunspaceIsRemote(Runspace runspace); - Task> PredictInput(Ast ast, Token[] tokens); + Task> PredictInputAsync(Ast ast, Token[] tokens); void OnCommandLineAccepted(IReadOnlyList history); + void OnCommandLineExecuted(string commandLine, bool success); void OnSuggestionDisplayed(Guid predictorId, uint session, int countOrIndex); void OnSuggestionAccepted(Guid predictorId, uint session, string suggestionText); void RenderFullHelp(string content, string regexPatternToScrollTo); diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index c3912371a..94e44204a 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -65,6 +65,7 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods private readonly StringBuilder _statusBuffer; private bool _statusIsErrorMessage; private string _statusLinePrompt; + private string _acceptedCommandLine; private List _edits; private int _editGroupStart; private int _undoEditIndex; @@ -302,11 +303,11 @@ private void PrependQueuedKeys(PSKeyInfo key) /// after the prompt has been displayed. /// /// The complete command line. - public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsics) + public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsics, bool? lastRunStatus) { // Use a default cancellation token instead of CancellationToken.None because the // WaitHandle is shared and could be triggered accidently. - return ReadLine(runspace, engineIntrinsics, _defaultCancellationToken); + return ReadLine(runspace, engineIntrinsics, _defaultCancellationToken, lastRunStatus); } /// @@ -314,7 +315,11 @@ public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsi /// ability to cancel ReadLine. /// /// The complete command line. - public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsics, CancellationToken cancellationToken) + public static string ReadLine( + Runspace runspace, + EngineIntrinsics engineIntrinsics, + CancellationToken cancellationToken, + bool? lastRunStatus) { var console = _singleton._console; @@ -348,6 +353,11 @@ public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsi catch {} } + if (lastRunStatus.HasValue) + { + _singleton.ReportExecutionStatus(lastRunStatus.Value); + } + bool firstTime = true; while (true) { @@ -486,11 +496,11 @@ private string InputLoop() ProcessOneKey(key, _dispatchTable, ignoreIfNoAction: false, arg: null); if (_inputAccepted) { - var commandLine = _buffer.ToString(); - MaybeAddToHistory(commandLine, _edits, _undoEditIndex); + _acceptedCommandLine = _buffer.ToString(); + MaybeAddToHistory(_acceptedCommandLine, _edits, _undoEditIndex); - _prediction.ActiveView.OnCommandLineAccepted(commandLine); - return commandLine; + _prediction.OnCommandLineAccepted(_acceptedCommandLine); + return _acceptedCommandLine; } if (killCommandCount == _killCommandCount) diff --git a/Polyfill/CommandPrediction.cs b/Polyfill/CommandPrediction.cs index 3b0c18aa6..e7b331a8b 100644 --- a/Polyfill/CommandPrediction.cs +++ b/Polyfill/CommandPrediction.cs @@ -4,8 +4,54 @@ using System.Threading.Tasks; using System.Management.Automation.Language; -namespace System.Management.Automation.Subsystem +namespace System.Management.Automation.Subsystem.Prediction { + /// + /// Kinds of prediction clients. + /// + public enum PredictionClientKind + { + /// + /// A terminal client, representing the command-line experience. + /// + Terminal, + + /// + /// An editor client, representing the editor experience. + /// + Editor, + } + + /// + /// The class represents a client that interacts with predictors. + /// + public sealed class PredictionClient + { + /// + /// Gets the client name. + /// + [HiddenAttribute] + public string Name { get; } + + /// + /// Gets the client kind. + /// + [HiddenAttribute] + public PredictionClientKind Kind { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Name of the interactive client. + /// Kind of the interactive client. + [HiddenAttribute] + public PredictionClient(string name, PredictionClientKind kind) + { + Name = name; + Kind = kind; + } + } + /// /// The class represents the prediction result from a predictor. /// @@ -103,7 +149,7 @@ public static class CommandPrediction /// The objects from parsing the current command line input. /// A list of objects. [HiddenAttribute] - public static Task> PredictInput(string client, Ast ast, Token[] astTokens) + public static Task> PredictInputAsync(PredictionClient client, Ast ast, Token[] astTokens) { return null; } @@ -117,7 +163,7 @@ public static Task> PredictInput(string client, Ast ast, /// The milliseconds to timeout. /// A list of objects. [HiddenAttribute] - public static Task> PredictInput(string client, Ast ast, Token[] astTokens, int millisecondsTimeout) + public static Task> PredictInputAsync(PredictionClient client, Ast ast, Token[] astTokens, int millisecondsTimeout) { return null; } @@ -128,7 +174,18 @@ public static Task> PredictInput(string client, Ast ast, /// Represents the client that initiates the call. /// History command lines provided as references for prediction. [HiddenAttribute] - public static void OnCommandLineAccepted(string client, IReadOnlyList history) + public static void OnCommandLineAccepted(PredictionClient client, IReadOnlyList history) + { + } + + /// + /// Allow registered predictors to know the execution result (success/failure) of the last accepted command line. + /// + /// Represents the client that initiates the call. + /// The last accepted command line. + /// Whether the execution of the last command line was successful. + [HiddenAttribute] + public static void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success) { } @@ -143,7 +200,7 @@ public static void OnCommandLineAccepted(string client, IReadOnlyList hi /// When the value is <= 0, it means a single suggestion from the list got displayed, and the index is the absolute value. /// [HiddenAttribute] - public static void OnSuggestionDisplayed(string client, Guid predictorId, uint session, int countOrIndex) + public static void OnSuggestionDisplayed(PredictionClient client, Guid predictorId, uint session, int countOrIndex) { } @@ -155,7 +212,7 @@ public static void OnSuggestionDisplayed(string client, Guid predictorId, uint s /// The mini-session where the accepted suggestion came from. /// The accepted suggestion text. [HiddenAttribute] - public static void OnSuggestionAccepted(string client, Guid predictorId, uint session, string suggestionText) + public static void OnSuggestionAccepted(PredictionClient client, Guid predictorId, uint session, string suggestionText) { } } @@ -163,9 +220,11 @@ public static void OnSuggestionAccepted(string client, Guid predictorId, uint se #else -using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.Prediction; using System.Runtime.CompilerServices; +[assembly: TypeForwardedTo(typeof(PredictionClientKind))] +[assembly: TypeForwardedTo(typeof(PredictionClient))] [assembly: TypeForwardedTo(typeof(PredictiveSuggestion))] [assembly: TypeForwardedTo(typeof(PredictionResult))] [assembly: TypeForwardedTo(typeof(CommandPrediction))] diff --git a/Polyfill/Polyfill.csproj b/Polyfill/Polyfill.csproj index 1e0ff4d44..c8334056e 100644 --- a/Polyfill/Polyfill.csproj +++ b/Polyfill/Polyfill.csproj @@ -12,7 +12,7 @@ - + diff --git a/test/InlinePredictionTest.cs b/test/InlinePredictionTest.cs index 43fd485e9..b2986e302 100644 --- a/test/InlinePredictionTest.cs +++ b/test/InlinePredictionTest.cs @@ -2,7 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Management.Automation.Language; -using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.Prediction; using System.Reflection; using Microsoft.PowerShell; using Xunit; @@ -566,5 +566,136 @@ public void Inline_HistoryAndPluginSource_Acceptance() Assert.Equal(1, _mockedMethods.commandHistory.Count); Assert.Equal("netsh show me", _mockedMethods.commandHistory[0]); } + + [SkippableFact] + public void Inline_NoneSource_ExecutionStatus() + { + TestSetup(KeyMode.Cmd); + using var disp = SetPrediction(PredictionSource.None, PredictionViewStyle.InlineView); + + // The last accepted command line would be "yay" after this. + Test("yay", Keys("yay")); + _mockedMethods.ClearPredictionFields(); + + // We always pass in 'true' as the execution status of the last command line, + // and that feedback will be reported when + // 1. the plugin source is in use; + // 2. the last accepted command is not a whitespace string. + Test(" ", Keys( + // Since we set the prediction source to be 'None', this feedback won't be reported. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + " ")); + + Test("abc", Keys( + // The prediction source is 'None', and the last accepted command is a whitespace string, + // so this feedback won't be reported. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + "abc")); + + Assert.Null(_mockedMethods.lastCommandRunStatus); + } + + [SkippableFact] + public void Inline_HistorySource_ExecutionStatus() + { + TestSetup(KeyMode.Cmd); + using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView); + + // The last accepted command line would be "yay" after this. + Test("yay", Keys("yay")); + _mockedMethods.ClearPredictionFields(); + + // We always pass in 'true' as the execution status of the last command line, + // and that feedback will be reported when + // 1. the plugin source is in use; + // 2. the last accepted command is not a whitespace string. + Test(" ", Keys( + // Since we set the prediction source to be 'History', this feedback won't be reported. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + " ")); + + Test("abc", Keys( + // The plugin source is in use, but the last accepted command is a whitespace string. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + "abc")); + + Assert.Null(_mockedMethods.lastCommandRunStatus); + } + + [SkippableFact] + public void Inline_PluginSource_ExecutionStatus() + { + TestSetup(KeyMode.Cmd); + using var disp = SetPrediction(PredictionSource.Plugin, PredictionViewStyle.InlineView); + + // The last accepted command line would be an empty string after this. + Test("", Keys(_.Enter)); + _mockedMethods.ClearPredictionFields(); + + // We always pass in 'true' as the execution status of the last command line, + // and that feedback will be reported when + // 1. the plugin source is in use; + // 2. the last accepted command is not a whitespace string. + Test("yay", Keys( + // The plugin source is in use, but the last accepted command is an empty string. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + "yay")); + + Assert.Null(_mockedMethods.lastCommandRunStatus); + + // The last accepted command line would be a whitespace string with 3 space characters after this. + Test(" ", Keys( + // The plugin source is in use, and the last accepted command is "yay". + CheckThat(() => Assert.True(_mockedMethods.lastCommandRunStatus)), + " ")); + + Assert.True(_mockedMethods.lastCommandRunStatus); + _mockedMethods.ClearPredictionFields(); + + Test("abc", Keys( + // The plugin source is in use, but the last accepted command is a whitespace string. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + "abc")); + + Assert.Null(_mockedMethods.lastCommandRunStatus); + } + + [SkippableFact] + public void Inline_HistoryAndPluginSource_ExecutionStatus() + { + TestSetup(KeyMode.Cmd); + using var disp = SetPrediction(PredictionSource.HistoryAndPlugin, PredictionViewStyle.InlineView); + + // The last accepted command line would be an empty string after this. + Test("", Keys(_.Enter)); + _mockedMethods.ClearPredictionFields(); + + // We always pass in 'true' as the execution status of the last command line, + // and that feedback will be reported when + // 1. the plugin source is in use; + // 2. the last accepted command is not a whitespace string. + Test("yay", Keys( + // The plugin source is in use, but the last accepted command is an empty string. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + "yay")); + + Assert.Null(_mockedMethods.lastCommandRunStatus); + + // The last accepted command line would be a whitespace string with 3 space characters after this. + Test(" ", Keys( + // The plugin source is in use, and the last accepted command is "yay". + CheckThat(() => Assert.True(_mockedMethods.lastCommandRunStatus)), + " ")); + + Assert.True(_mockedMethods.lastCommandRunStatus); + _mockedMethods.ClearPredictionFields(); + + Test("abc", Keys( + // The plugin source is in use, but the last accepted command is a whitespace string. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + "abc")); + + Assert.Null(_mockedMethods.lastCommandRunStatus); + } } } diff --git a/test/ListPredictionTest.cs b/test/ListPredictionTest.cs index 82c6978dd..0bb5afd55 100644 --- a/test/ListPredictionTest.cs +++ b/test/ListPredictionTest.cs @@ -1509,5 +1509,137 @@ public void List_HistoryAndPluginSource_Acceptance() Assert.Equal("eca -zoo", _mockedMethods.commandHistory[2]); Assert.Equal("SOME NEW TEX SOME TEXT AFTER", _mockedMethods.commandHistory[3]); } + + [SkippableFact] + public void List_NoneSource_ExecutionStatus() + { + TestSetup(KeyMode.Cmd); + using var disp = SetPrediction(PredictionSource.None, PredictionViewStyle.ListView); + + // The last accepted command line would be "yay" after this. + Test("yay", Keys("yay")); + _mockedMethods.ClearPredictionFields(); + + // We always pass in 'true' as the execution status of the last command line, + // and that feedback will be reported when + // 1. the plugin source is in use; + // 2. the last accepted command is not a whitespace string. + Test(" ", Keys( + // Since we set the prediction source to be 'None', this feedback won't be reported. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + " ")); + + Test("abc", Keys( + // The prediction source is 'None', and the last accepted command is a whitespace string, + // so this feedback won't be reported. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + "abc")); + + Assert.Null(_mockedMethods.lastCommandRunStatus); + } + + [SkippableFact] + public void List_HistorySource_ExecutionStatus() + { + TestSetup(KeyMode.Cmd); + using var disp = SetPrediction(PredictionSource.History, PredictionViewStyle.InlineView); + + // The last accepted command line would be "yay" after this. + Test("yay", Keys("yay")); + _mockedMethods.ClearPredictionFields(); + + // We always pass in 'true' as the execution status of the last command line, + // and that feedback will be reported when + // 1. the plugin source is in use; + // 2. the last accepted command is not a whitespace string. + Test(" ", Keys( + // Since we set the prediction source to be 'History', this feedback won't be reported. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + " ")); + + Test("abc", Keys( + // The prediction source is 'History', and the last accepted command is a whitespace string, + // so this feedback won't be reported. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + "abc")); + + Assert.Null(_mockedMethods.lastCommandRunStatus); + } + + [SkippableFact] + public void List_PluginSource_ExecutionStatus() + { + TestSetup(KeyMode.Cmd); + using var disp = SetPrediction(PredictionSource.Plugin, PredictionViewStyle.InlineView); + + // The last accepted command line would be an empty string after this. + Test("", Keys(_.Enter)); + _mockedMethods.ClearPredictionFields(); + + // We always pass in 'true' as the execution status of the last command line, + // and that feedback will be reported when + // 1. the plugin source is in use; + // 2. the last accepted command is not a whitespace string. + Test("yay", Keys( + // The plugin source is in use, but the last accepted command is an empty string. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + "yay")); + + Assert.Null(_mockedMethods.lastCommandRunStatus); + + // The last accepted command line would be a whitespace string with 3 space characters after this. + Test(" ", Keys( + // The plugin source is in use, and the last accepted command is "yay". + CheckThat(() => Assert.True(_mockedMethods.lastCommandRunStatus)), + " ")); + + Assert.True(_mockedMethods.lastCommandRunStatus); + _mockedMethods.ClearPredictionFields(); + + Test("abc", Keys( + // The plugin source is in use, but the last accepted command is a whitespace string. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + "abc")); + + Assert.Null(_mockedMethods.lastCommandRunStatus); + } + + [SkippableFact] + public void List_HistoryAndPluginSource_ExecutionStatus() + { + TestSetup(KeyMode.Cmd); + using var disp = SetPrediction(PredictionSource.HistoryAndPlugin, PredictionViewStyle.InlineView); + + // The last accepted command line would be an empty string after this. + Test("", Keys(_.Enter)); + _mockedMethods.ClearPredictionFields(); + + // We always pass in 'true' as the execution status of the last command line, + // and that feedback will be reported when + // 1. the plugin source is in use; + // 2. the last accepted command is not a whitespace string. + Test("yay", Keys( + // The plugin source is in use, but the last accepted command is an empty string. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + "yay")); + + Assert.Null(_mockedMethods.lastCommandRunStatus); + + // The last accepted command line would be a whitespace string with 3 space characters after this. + Test(" ", Keys( + // The plugin source is in use, and the last accepted command is "yay". + CheckThat(() => Assert.True(_mockedMethods.lastCommandRunStatus)), + " ")); + + Assert.True(_mockedMethods.lastCommandRunStatus); + _mockedMethods.ClearPredictionFields(); + + Test("abc", Keys( + // The plugin source is in use, but the last accepted command is a whitespace string. + CheckThat(() => Assert.Null(_mockedMethods.lastCommandRunStatus)), + "abc")); + + Assert.Null(_mockedMethods.lastCommandRunStatus); + } } } diff --git a/test/PSReadLine.Tests.csproj b/test/PSReadLine.Tests.csproj index f89604daa..f6de2e71f 100644 --- a/test/PSReadLine.Tests.csproj +++ b/test/PSReadLine.Tests.csproj @@ -24,7 +24,7 @@ - + diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index e3c5dd06b..f4587285b 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -6,7 +6,7 @@ using System.Management.Automation; using System.Management.Automation.Language; using System.Management.Automation.Runspaces; -using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.Prediction; using System.Threading.Tasks; using System.Reflection; using System.Runtime.InteropServices; @@ -23,6 +23,7 @@ internal class MockedMethods : IPSConsoleReadLineMockableMethods { internal bool didDing; internal IReadOnlyList commandHistory; + internal bool? lastCommandRunStatus; internal Guid acceptedPredictorId; internal string acceptedSuggestion; internal string helpContentRendered; @@ -31,6 +32,7 @@ internal class MockedMethods : IPSConsoleReadLineMockableMethods internal void ClearPredictionFields() { commandHistory = null; + lastCommandRunStatus = null; acceptedPredictorId = Guid.Empty; acceptedSuggestion = null; displayedSuggestions.Clear(); @@ -51,7 +53,7 @@ public bool RunspaceIsRemote(Runspace runspace) return false; } - public Task> PredictInput(Ast ast, Token[] tokens) + public Task> PredictInputAsync(Ast ast, Token[] tokens) { var result = ReadLine.MockedPredictInput(ast, tokens); var source = new TaskCompletionSource>(); @@ -65,6 +67,11 @@ public void OnCommandLineAccepted(IReadOnlyList history) commandHistory = history; } + public void OnCommandLineExecuted(string commandLine, bool success) + { + lastCommandRunStatus = success; + } + public void OnSuggestionDisplayed(Guid predictorId, uint session, int countOrIndex) { displayedSuggestions[predictorId] = Tuple.Create(session, countOrIndex); @@ -474,7 +481,10 @@ private void Test(string expectedResult, object[] items, bool resetCursor, strin _console.Init(items); - var result = PSConsoleReadLine.ReadLine(null, null); + var result = PSConsoleReadLine.ReadLine( + runspace: null, + engineIntrinsics: null, + lastRunStatus: true); if (_console.validationFailure != null) {