From 1e12b1dcfb519de85405aabf24f95bcabcc95001 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 17 May 2021 10:11:15 -0700 Subject: [PATCH 1/8] Update command prediction interface --- .../CommandPrediction/CommandPrediction.cs | 114 +++++++++++++---- .../CommandPrediction/ICommandPredictor.cs | 116 ++++++++++++++---- test/xUnit/csharp/test_Prediction.cs | 103 ++++++++++------ test/xUnit/csharp/test_Subsystem.cs | 3 +- 4 files changed, 251 insertions(+), 85 deletions(-) diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs index edc8fb272a2..bab064bd46d 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs @@ -59,7 +59,7 @@ public static class CommandPrediction /// The object from parsing the current command line input. /// The objects from parsing the current command line input. /// A list of objects. - public static Task?> PredictInput(string client, Ast ast, Token[] astTokens) + public static Task?> PredictInput(PredictionClient client, Ast ast, Token[] astTokens) { return PredictInput(client, ast, astTokens, millisecondsTimeout: 20); } @@ -72,7 +72,7 @@ public static class CommandPrediction /// The objects from parsing the current command line input. /// The milliseconds to timeout. /// A list of objects. - public static async Task?> PredictInput(string client, Ast ast, Token[] astTokens, int millisecondsTimeout) + public static async Task?> PredictInput(PredictionClient client, Ast ast, Token[] astTokens, int millisecondsTimeout) { Requires.Condition(millisecondsTimeout > 0, nameof(millisecondsTimeout)); @@ -86,17 +86,13 @@ public static class CommandPrediction var tasks = new Task[predictors.Count]; using var cancellationSource = new CancellationTokenSource(); + Func callBack = GetCallBack(client, context, cancellationSource); + for (int i = 0; i < predictors.Count; i++) { ICommandPredictor predictor = predictors[i]; - tasks[i] = Task.Factory.StartNew( - state => - { - var predictor = (ICommandPredictor)state!; - SuggestionPackage pkg = predictor.GetSuggestion(client, context, cancellationSource.Token); - return pkg.SuggestionEntries?.Count > 0 ? new PredictionResult(predictor.Id, predictor.Name, pkg.Session, pkg.SuggestionEntries) : null; - }, + callBack, predictor, cancellationSource.Token, TaskCreationOptions.DenyChildAttach, @@ -122,6 +118,21 @@ await Task.WhenAny( } return resultList; + + // A local helper function to avoid creating an instance of the generated delegate helper class + // when no predictor is registered. + static Func GetCallBack( + PredictionClient client, + PredictionContext context, + CancellationTokenSource cancellationSource) + { + return state => + { + var predictor = (ICommandPredictor)state!; + SuggestionPackage pkg = predictor.GetSuggestion(client, context, cancellationSource.Token); + return pkg.SuggestionEntries?.Count > 0 ? new PredictionResult(predictor.Id, predictor.Name, pkg.Session, pkg.SuggestionEntries) : null; + }; + } } /// @@ -129,7 +140,7 @@ await Task.WhenAny( /// /// Represents the client that initiates the call. /// History command lines provided as references for prediction. - public static void OnCommandLineAccepted(string client, IReadOnlyList history) + public static void OnCommandLineAccepted(PredictionClient client, IReadOnlyList history) { Requires.NotNull(history, nameof(history)); @@ -139,16 +150,53 @@ public static void OnCommandLineAccepted(string client, IReadOnlyList hi return; } + Action? callBack = null; foreach (ICommandPredictor predictor in predictors) { - if (predictor.SupportEarlyProcessing) + if (predictor.AcceptFeedback(client, PredictorFeedback.OnCommandLineAccepted)) { - ThreadPool.QueueUserWorkItem( - state => state.StartEarlyProcessing(client, history), - predictor, - preferLocal: false); + callBack ??= GetCallBack(client, history); + ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); } } + + // A local helper function to avoid creating an instance of the generated delegate helper class + // when no predictor is registered, or no registered predictor accepts this feedback. + static Action GetCallBack(PredictionClient client, IReadOnlyList history) + { + return state => state.OnCommandLineAccepted(client, history); + } + } + + /// + /// Allow registered predictors to know the execution result (success/failure) of the last accepted command line. + /// + /// + /// + public static void OnCommandLineExecuted(PredictionClient client, bool status) + { + var predictors = SubsystemManager.GetSubsystems(); + if (predictors.Count == 0) + { + return; + } + + Action? callBack = null; + foreach (ICommandPredictor predictor in predictors) + { + if (predictor.AcceptFeedback(client, PredictorFeedback.OnCommandLineExecuted)) + { + callBack ??= GetCallBack(client, status); + ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); + } + } + + // A local helper function to avoid creating an instance of the generated delegate helper class + // when no predictor is registered, or no registered predictor accepts this feedback. + static Action GetCallBack(PredictionClient client, bool status) + { + return state => state.OnCommandLineExecuted(client, status); + } } /// @@ -161,7 +209,7 @@ public static void OnCommandLineAccepted(string client, IReadOnlyList hi /// When the value is greater than 0, it's the number of displayed suggestions from the list returned in , starting from the index 0. /// When the value is less than or equal to 0, it means a single suggestion from the list got displayed, and the index is the absolute value. /// - public static void OnSuggestionDisplayed(string client, Guid predictorId, uint session, int countOrIndex) + public static void OnSuggestionDisplayed(PredictionClient client, Guid predictorId, uint session, int countOrIndex) { var predictors = SubsystemManager.GetSubsystems(); if (predictors.Count == 0) @@ -169,16 +217,22 @@ public static void OnSuggestionDisplayed(string client, Guid predictorId, uint s return; } + Action? callBack = null; foreach (ICommandPredictor predictor in predictors) { - if (predictor.AcceptFeedback && predictor.Id == predictorId) + if (predictor.Id == predictorId && predictor.AcceptFeedback(client, PredictorFeedback.OnSuggestionDisplayed)) { - ThreadPool.QueueUserWorkItem( - state => state.OnSuggestionDisplayed(client, session, countOrIndex), - predictor, - preferLocal: false); + callBack ??= GetCallBack(client, session, countOrIndex); + ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); } } + + // A local helper function to avoid creating an instance of the generated delegate helper class + // when no predictor is registered, or no registered predictor accepts this feedback. + static Action GetCallBack(PredictionClient client, uint session, int countOrIndex) + { + return state => state.OnSuggestionDisplayed(client, session, countOrIndex); + } } /// @@ -188,7 +242,7 @@ public static void OnSuggestionDisplayed(string client, Guid predictorId, uint s /// The identifier of the predictor whose prediction result was accepted. /// The mini-session where the accepted suggestion came from. /// The accepted suggestion text. - public static void OnSuggestionAccepted(string client, Guid predictorId, uint session, string suggestionText) + public static void OnSuggestionAccepted(PredictionClient client, Guid predictorId, uint session, string suggestionText) { Requires.NotNullOrEmpty(suggestionText, nameof(suggestionText)); @@ -198,16 +252,22 @@ public static void OnSuggestionAccepted(string client, Guid predictorId, uint se return; } + Action? callBack = null; foreach (ICommandPredictor predictor in predictors) { - if (predictor.AcceptFeedback && predictor.Id == predictorId) + if (predictor.Id == predictorId && predictor.AcceptFeedback(client, PredictorFeedback.OnSuggestionAccepted)) { - ThreadPool.QueueUserWorkItem( - state => state.OnSuggestionAccepted(client, session, suggestionText), - predictor, - preferLocal: false); + callBack ??= GetCallBack(client, session, suggestionText); + ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); } } + + // A local helper function to avoid creating an instance of the generated delegate helper class + // when no predictor is registered, or no registered predictor accepts this feedback. + static Action GetCallBack(PredictionClient client, uint session, string suggestionText) + { + return state => state.OnSuggestionAccepted(client, session, suggestionText); + } } } } diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs index d55d37a90df..19e58903c3b 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs @@ -27,50 +27,124 @@ public interface ICommandPredictor : ISubsystem SubsystemKind ISubsystem.Kind => SubsystemKind.CommandPredictor; /// - /// Gets a value indicating whether the predictor supports early processing. + /// Gets a value indicating whether the predictor accepts a specific feedback. /// - bool SupportEarlyProcessing { get; } - - /// - /// Gets a value indicating whether the predictor accepts feedback about the previous suggestion. - /// - bool AcceptFeedback { get; } - - /// - /// A command line was accepted to execute. - /// The predictor can start processing early as needed with the latest history. - /// - /// Represents the client that initiates the call. - /// History command lines provided as references for prediction. - void StartEarlyProcessing(string clientId, IReadOnlyList history); + /// + /// + /// + bool AcceptFeedback(PredictionClient client, PredictorFeedback feedback); /// /// Get the predictive suggestions. It indicates the start of a suggestion rendering session. /// - /// Represents the client that initiates the call. + /// Represents the client that initiates the call. /// The object to be used for prediction. /// The cancellation token to cancel the prediction. /// An instance of . - SuggestionPackage GetSuggestion(string clientId, PredictionContext context, CancellationToken cancellationToken); + SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken); /// /// One or more suggestions provided by the predictor were displayed to the user. /// - /// Represents the client that initiates the call. + /// Represents the client that initiates the call. /// The mini-session where the displayed suggestions came from. /// /// When the value is greater than 0, it's the number of displayed suggestions from the list returned in , starting from the index 0. /// When the value is less than or equal to 0, it means a single suggestion from the list got displayed, and the index is the absolute value. /// - void OnSuggestionDisplayed(string clientId, uint session, int countOrIndex); + void OnSuggestionDisplayed(PredictionClient client, uint session, int countOrIndex); /// /// The suggestion provided by the predictor was accepted. /// - /// Represents the client that initiates the call. + /// Represents the client that initiates the call. /// Represents the mini-session where the accepted suggestion came from. /// The accepted suggestion text. - void OnSuggestionAccepted(string clientId, uint session, string acceptedSuggestion); + void OnSuggestionAccepted(PredictionClient client, uint session, string acceptedSuggestion); + + /// + /// A command line was accepted to execute. + /// The predictor can start processing early as needed with the latest history. + /// + /// Represents the client that initiates the call. + /// History command lines provided as references for prediction. + void OnCommandLineAccepted(PredictionClient client, IReadOnlyList history); + + /// + /// A command line was done execution. + /// + /// Represents the client that initiates the call. + /// Shows whether the execution succeeded or failed. + void OnCommandLineExecuted(PredictionClient client, bool status); + } + + /// + /// Kinds of feedback a predictor can choose to accept. + /// + public enum PredictorFeedback + { + /// + /// Feedback when one or more suggestions are displayed to the user. + /// + OnSuggestionDisplayed, + + /// + /// Feedback when a suggestion is accepted by the user. + /// + OnSuggestionAccepted, + + /// + /// Feedback when a command line is accepted by the user. + /// + OnCommandLineAccepted, + + /// + /// Feedback when the accepted command line finishes its execution. + /// + OnCommandLineExecuted, + } + + /// + /// The type that represents a client that interacts with predictors. + /// + public class PredictionClient + { + /// + /// Kinds of the client. + /// + public enum ClientKind + { + /// + /// A terminal client, representing the command-line experience. + /// + Terminal, + + /// + /// An editor client, representing the editor experience. + /// + Editor, + } + + /// + /// Gets the client name. + /// + public string Name { get; } + + /// + /// Gets the client kind. + /// + public ClientKind Kind { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Name of the interactive client. + /// Kind of the interactive client. + public PredictionClient(string name, ClientKind kind) + { + Name = name; + Kind = kind; + } } /// diff --git a/test/xUnit/csharp/test_Prediction.cs b/test/xUnit/csharp/test_Prediction.cs index 8f018ebb9e1..771343be4df 100644 --- a/test/xUnit/csharp/test_Prediction.cs +++ b/test/xUnit/csharp/test_Prediction.cs @@ -18,6 +18,8 @@ public class MyPredictor : ICommandPredictor public List History { get; } + public List Results { get; } + public List AcceptedSuggestions { get; } public List DisplayedSuggestions { get; } @@ -47,39 +49,30 @@ private MyPredictor(Guid id, string name, string description, bool delay) _delay = delay; History = new List(); + Results = new List(); AcceptedSuggestions = new List(); DisplayedSuggestions = new List(); } + public void Clear() + { + History.Clear(); + Results.Clear(); + AcceptedSuggestions.Clear(); + DisplayedSuggestions.Clear(); + } + + #region "Interface implementation" + public Guid Id => _id; public string Name => _name; public string Description => _description; - bool ICommandPredictor.SupportEarlyProcessing => true; + bool ICommandPredictor.AcceptFeedback(PredictionClient client, PredictorFeedback feedback) => true; - bool ICommandPredictor.AcceptFeedback => true; - - public void StartEarlyProcessing(string clientId, IReadOnlyList history) - { - foreach (string item in history) - { - History.Add($"{clientId}-{item}"); - } - } - - public void OnSuggestionDisplayed(string clientId, uint session, int countOrIndex) - { - DisplayedSuggestions.Add($"{clientId}-{session}-{countOrIndex}"); - } - - public void OnSuggestionAccepted(string clientId, uint session, string acceptedSuggestion) - { - AcceptedSuggestions.Add($"{clientId}-{session}-{acceptedSuggestion}"); - } - - public SuggestionPackage GetSuggestion(string clientId, PredictionContext context, CancellationToken cancellationToken) + public SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken) { if (_delay) { @@ -92,18 +85,44 @@ public SuggestionPackage GetSuggestion(string clientId, PredictionContext contex var userInput = context.InputAst.Extent.Text; var entries = new List { - new PredictiveSuggestion($"'{userInput}' from '{clientId}' - TEST-1 from {Name}"), - new PredictiveSuggestion($"'{userInput}' from '{clientId}' - TeSt-2 from {Name}"), + new PredictiveSuggestion($"'{userInput}' from '{client.Name}' - TEST-1 from {Name}"), + new PredictiveSuggestion($"'{userInput}' from '{client.Name}' - TeSt-2 from {Name}"), }; return new SuggestionPackage(56, entries); } + + public void OnSuggestionDisplayed(PredictionClient client, uint session, int countOrIndex) + { + DisplayedSuggestions.Add($"{client.Name}-{session}-{countOrIndex}"); + } + + public void OnSuggestionAccepted(PredictionClient client, uint session, string acceptedSuggestion) + { + AcceptedSuggestions.Add($"{client.Name}-{session}-{acceptedSuggestion}"); + } + + public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList history) + { + foreach (string item in history) + { + History.Add($"{client.Name}-{item}"); + } + } + + public void OnCommandLineExecuted(PredictionClient client, bool status) + { + Results.Add($"{client.Name}-{status}"); + } + + #endregion } public static class CommandPredictionTests { private const string Client = "PredictionTest"; private const uint Session = 56; + private static PredictionClient predClient = new(Client, PredictionClient.ClientKind.Terminal); [Fact] public static void PredictInput() @@ -114,7 +133,7 @@ public static void PredictInput() Ast ast = Parser.ParseInput(Input, out Token[] tokens, out _); // Returns null when no predictor implementation registered - List results = CommandPrediction.PredictInput(Client, ast, tokens).Result; + List results = CommandPrediction.PredictInput(predClient, ast, tokens).Result; Assert.Null(results); try @@ -127,7 +146,7 @@ public static void PredictInput() // cannot finish before the specified timeout. // The specified timeout is exaggerated to make the test reliable. // xUnit must spin up a lot tasks, which makes the test unreliable when the time difference between 'delay' and 'timeout' is small. - results = CommandPrediction.PredictInput(Client, ast, tokens, millisecondsTimeout: 1000).Result; + results = CommandPrediction.PredictInput(predClient, ast, tokens, millisecondsTimeout: 1000).Result; Assert.Single(results); PredictionResult res = results[0]; @@ -140,7 +159,7 @@ public static void PredictInput() // Expect the results from both 'slow' and 'fast' predictors // Same here -- the specified timeout is exaggerated to make the test reliable. // xUnit must spin up a lot tasks, which makes the test unreliable when the time difference between 'delay' and 'timeout' is small. - results = CommandPrediction.PredictInput(Client, ast, tokens, millisecondsTimeout: 4000).Result; + results = CommandPrediction.PredictInput(predClient, ast, tokens, millisecondsTimeout: 4000).Result; Assert.Equal(2, results.Count); PredictionResult res1 = results[0]; @@ -170,6 +189,9 @@ public static void Feedback() MyPredictor slow = MyPredictor.SlowPredictor; MyPredictor fast = MyPredictor.FastPredictor; + slow.Clear(); + fast.Clear(); + try { // Register 2 predictor implementations @@ -179,16 +201,19 @@ public static void Feedback() var history = new[] { "hello", "world" }; var ids = new HashSet { slow.Id, fast.Id }; - CommandPrediction.OnCommandLineAccepted(Client, history); - CommandPrediction.OnSuggestionDisplayed(Client, slow.Id, Session, 2); - CommandPrediction.OnSuggestionDisplayed(Client, fast.Id, Session, -1); - CommandPrediction.OnSuggestionAccepted(Client, slow.Id, Session, "Yeah"); - - // The calls to 'StartEarlyProcessing' and 'OnSuggestionAccepted' are queued in thread pool, - // so we wait a bit to make sure the calls are done. - while (slow.History.Count == 0 || slow.AcceptedSuggestions.Count == 0) + CommandPrediction.OnCommandLineAccepted(predClient, history); + CommandPrediction.OnCommandLineExecuted(predClient, true); + CommandPrediction.OnSuggestionDisplayed(predClient, slow.Id, Session, 2); + CommandPrediction.OnSuggestionDisplayed(predClient, fast.Id, Session, -1); + CommandPrediction.OnSuggestionAccepted(predClient, slow.Id, Session, "Yeah"); + + // The feedback calls are queued in thread pool, so let's wait a bit to make sure the calls are done. + while (slow.History.Count == 0 || fast.History.Count == 0 || + slow.Results.Count == 0 || fast.Results.Count == 0 || + slow.DisplayedSuggestions.Count == 0 || fast.DisplayedSuggestions.Count == 0 || + slow.AcceptedSuggestions.Count == 0) { - Thread.Sleep(10); + Thread.Sleep(100); } Assert.Equal(2, slow.History.Count); @@ -199,6 +224,12 @@ public static void Feedback() Assert.Equal($"{Client}-{history[0]}", fast.History[0]); Assert.Equal($"{Client}-{history[1]}", fast.History[1]); + Assert.Single(slow.Results); + Assert.Equal($"{Client}-True", slow.Results[0]); + + Assert.Single(fast.Results); + Assert.Equal($"{Client}-True", fast.Results[0]); + Assert.Single(slow.DisplayedSuggestions); Assert.Equal($"{Client}-{Session}-2", slow.DisplayedSuggestions[0]); diff --git a/test/xUnit/csharp/test_Subsystem.cs b/test/xUnit/csharp/test_Subsystem.cs index f2ca308221f..f54daa45eeb 100644 --- a/test/xUnit/csharp/test_Subsystem.cs +++ b/test/xUnit/csharp/test_Subsystem.cs @@ -97,8 +97,9 @@ public static void RegisterSubsystem() const string Client = "SubsystemTest"; const string Input = "Hello world"; + var predClient = new PredictionClient(Client, PredictionClient.ClientKind.Terminal); var predCxt = PredictionContext.Create(Input); - var results = impl.GetSuggestion(Client, predCxt, CancellationToken.None); + var results = impl.GetSuggestion(predClient, predCxt, CancellationToken.None); Assert.Equal($"'{Input}' from '{Client}' - TEST-1 from {impl.Name}", results.SuggestionEntries[0].SuggestionText); Assert.Equal($"'{Input}' from '{Client}' - TeSt-2 from {impl.Name}", results.SuggestionEntries[1].SuggestionText); From 5e0c5b0d30909b1f5a3ac4a13e080b4d19edb9ee Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 17 May 2021 14:57:01 -0700 Subject: [PATCH 2/8] Update the interface API --- .../CommandPrediction/CommandPrediction.cs | 13 +++++++------ .../CommandPrediction/ICommandPredictor.cs | 5 +++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs index bab064bd46d..e73b25e5cf7 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs @@ -171,9 +171,10 @@ static Action GetCallBack(PredictionClient client, IReadOnlyL /// /// Allow registered predictors to know the execution result (success/failure) of the last accepted command line. /// - /// - /// - public static void OnCommandLineExecuted(PredictionClient client, bool status) + /// Represents the client that initiates the call. + /// The last accepted command line. + /// The execution status of the last command line. True for success, False for failure + public static void OnCommandLineExecuted(PredictionClient client, string commandLine, bool status) { var predictors = SubsystemManager.GetSubsystems(); if (predictors.Count == 0) @@ -186,16 +187,16 @@ public static void OnCommandLineExecuted(PredictionClient client, bool status) { if (predictor.AcceptFeedback(client, PredictorFeedback.OnCommandLineExecuted)) { - callBack ??= GetCallBack(client, status); + callBack ??= GetCallBack(client, commandLine, status); ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); } } // A local helper function to avoid creating an instance of the generated delegate helper class // when no predictor is registered, or no registered predictor accepts this feedback. - static Action GetCallBack(PredictionClient client, bool status) + static Action GetCallBack(PredictionClient client, string commandLine, bool status) { - return state => state.OnCommandLineExecuted(client, status); + return state => state.OnCommandLineExecuted(client, commandLine, status); } } diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs index 19e58903c3b..04612dc35ed 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs @@ -74,8 +74,9 @@ public interface ICommandPredictor : ISubsystem /// A command line was done execution. /// /// Represents the client that initiates the call. + /// The last accepted command line. /// Shows whether the execution succeeded or failed. - void OnCommandLineExecuted(PredictionClient client, bool status); + void OnCommandLineExecuted(PredictionClient client, string commandLine, bool status); } /// @@ -105,7 +106,7 @@ public enum PredictorFeedback } /// - /// The type that represents a client that interacts with predictors. + /// The class represents a client that interacts with predictors. /// public class PredictionClient { From c9680cc0f430f927fe55745a22e61821323f6b3a Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 18 May 2021 09:06:14 -0700 Subject: [PATCH 3/8] Minor update --- .../Subsystem/CommandPrediction/CommandPrediction.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs index e73b25e5cf7..a936157bcab 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs @@ -164,7 +164,7 @@ public static void OnCommandLineAccepted(PredictionClient client, IReadOnlyList< // when no predictor is registered, or no registered predictor accepts this feedback. static Action GetCallBack(PredictionClient client, IReadOnlyList history) { - return state => state.OnCommandLineAccepted(client, history); + return predictor => predictor.OnCommandLineAccepted(client, history); } } @@ -196,7 +196,7 @@ public static void OnCommandLineExecuted(PredictionClient client, string command // when no predictor is registered, or no registered predictor accepts this feedback. static Action GetCallBack(PredictionClient client, string commandLine, bool status) { - return state => state.OnCommandLineExecuted(client, commandLine, status); + return predictor => predictor.OnCommandLineExecuted(client, commandLine, status); } } @@ -232,7 +232,7 @@ public static void OnSuggestionDisplayed(PredictionClient client, Guid predictor // when no predictor is registered, or no registered predictor accepts this feedback. static Action GetCallBack(PredictionClient client, uint session, int countOrIndex) { - return state => state.OnSuggestionDisplayed(client, session, countOrIndex); + return predictor => predictor.OnSuggestionDisplayed(client, session, countOrIndex); } } @@ -267,7 +267,7 @@ public static void OnSuggestionAccepted(PredictionClient client, Guid predictorI // when no predictor is registered, or no registered predictor accepts this feedback. static Action GetCallBack(PredictionClient client, uint session, string suggestionText) { - return state => state.OnSuggestionAccepted(client, session, suggestionText); + return predictor => predictor.OnSuggestionAccepted(client, session, suggestionText); } } } From f2ebfff44d76b2153804e4574451cbfd5c86678a Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 18 May 2021 09:25:11 -0700 Subject: [PATCH 4/8] Update test --- test/xUnit/csharp/test_Prediction.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/xUnit/csharp/test_Prediction.cs b/test/xUnit/csharp/test_Prediction.cs index 771343be4df..7975d2a96f3 100644 --- a/test/xUnit/csharp/test_Prediction.cs +++ b/test/xUnit/csharp/test_Prediction.cs @@ -110,9 +110,9 @@ public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList } } - public void OnCommandLineExecuted(PredictionClient client, bool status) + public void OnCommandLineExecuted(PredictionClient client, string commandLine, bool status) { - Results.Add($"{client.Name}-{status}"); + Results.Add($"{client.Name}-{commandLine}-{status}"); } #endregion @@ -202,7 +202,7 @@ public static void Feedback() var ids = new HashSet { slow.Id, fast.Id }; CommandPrediction.OnCommandLineAccepted(predClient, history); - CommandPrediction.OnCommandLineExecuted(predClient, true); + CommandPrediction.OnCommandLineExecuted(predClient, "last_input", true); CommandPrediction.OnSuggestionDisplayed(predClient, slow.Id, Session, 2); CommandPrediction.OnSuggestionDisplayed(predClient, fast.Id, Session, -1); CommandPrediction.OnSuggestionAccepted(predClient, slow.Id, Session, "Yeah"); @@ -225,10 +225,10 @@ public static void Feedback() Assert.Equal($"{Client}-{history[1]}", fast.History[1]); Assert.Single(slow.Results); - Assert.Equal($"{Client}-True", slow.Results[0]); + Assert.Equal($"{Client}-last_input-True", slow.Results[0]); Assert.Single(fast.Results); - Assert.Equal($"{Client}-True", fast.Results[0]); + Assert.Equal($"{Client}-last_input-True", fast.Results[0]); Assert.Single(slow.DisplayedSuggestions); Assert.Equal($"{Client}-{Session}-2", slow.DisplayedSuggestions[0]); From 381564d497cffe638ef189fcd68ce2a2ff674d8f Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 18 May 2021 09:32:06 -0700 Subject: [PATCH 5/8] Fix CodeFactor issues --- .../engine/Subsystem/CommandPrediction/CommandPrediction.cs | 2 +- .../engine/Subsystem/CommandPrediction/ICommandPredictor.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs index a936157bcab..93b172b16dd 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs @@ -173,7 +173,7 @@ static Action GetCallBack(PredictionClient client, IReadOnlyL /// /// Represents the client that initiates the call. /// The last accepted command line. - /// The execution status of the last command line. True for success, False for failure + /// The execution status of the last command line. True for success, False for failure. public static void OnCommandLineExecuted(PredictionClient client, string commandLine, bool status) { var predictors = SubsystemManager.GetSubsystems(); diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs index 04612dc35ed..6fe31747060 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs @@ -29,9 +29,9 @@ public interface ICommandPredictor : ISubsystem /// /// Gets a value indicating whether the predictor accepts a specific feedback. /// - /// - /// - /// + /// Represents the client that initiates the call. + /// A specific type of feedback. + /// True or false, to indicate whether the specific feedback is accepted. bool AcceptFeedback(PredictionClient client, PredictorFeedback feedback); /// From da81c148d798d5e42b8c6943c740feef3171b378 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 18 May 2021 16:26:49 -0700 Subject: [PATCH 6/8] Address Rob's feedback --- .../CommandPrediction/CommandPrediction.cs | 40 ++++++----- .../CommandPrediction/ICommandPredictor.cs | 66 +++++++++---------- .../engine/Subsystem/SubsystemManager.cs | 1 + test/xUnit/csharp/test_Prediction.cs | 9 +-- test/xUnit/csharp/test_Subsystem.cs | 3 +- 5 files changed, 65 insertions(+), 54 deletions(-) diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs index 93b172b16dd..f9c91978d57 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs @@ -10,7 +10,7 @@ using System.Threading; using System.Threading.Tasks; -namespace System.Management.Automation.Subsystem +namespace System.Management.Automation.Subsystem.Prediction { /// /// The class represents the prediction result from a predictor. @@ -153,7 +153,7 @@ public static void OnCommandLineAccepted(PredictionClient client, IReadOnlyList< Action? callBack = null; foreach (ICommandPredictor predictor in predictors) { - if (predictor.AcceptFeedback(client, PredictorFeedback.OnCommandLineAccepted)) + if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.CommandLineAccepted)) { callBack ??= GetCallBack(client, history); ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); @@ -173,8 +173,8 @@ static Action GetCallBack(PredictionClient client, IReadOnlyL /// /// Represents the client that initiates the call. /// The last accepted command line. - /// The execution status of the last command line. True for success, False for failure. - public static void OnCommandLineExecuted(PredictionClient client, string commandLine, bool status) + /// Whether the execution of the last command line was successful. + public static void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success) { var predictors = SubsystemManager.GetSubsystems(); if (predictors.Count == 0) @@ -185,18 +185,18 @@ public static void OnCommandLineExecuted(PredictionClient client, string command Action? callBack = null; foreach (ICommandPredictor predictor in predictors) { - if (predictor.AcceptFeedback(client, PredictorFeedback.OnCommandLineExecuted)) + if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.CommandLineExecuted)) { - callBack ??= GetCallBack(client, commandLine, status); + callBack ??= GetCallBack(client, commandLine, success); ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); } } // A local helper function to avoid creating an instance of the generated delegate helper class // when no predictor is registered, or no registered predictor accepts this feedback. - static Action GetCallBack(PredictionClient client, string commandLine, bool status) + static Action GetCallBack(PredictionClient client, string commandLine, bool success) { - return predictor => predictor.OnCommandLineExecuted(client, commandLine, status); + return predictor => predictor.OnCommandLineExecuted(client, commandLine, success); } } @@ -218,13 +218,17 @@ public static void OnSuggestionDisplayed(PredictionClient client, Guid predictor return; } - Action? callBack = null; foreach (ICommandPredictor predictor in predictors) { - if (predictor.Id == predictorId && predictor.AcceptFeedback(client, PredictorFeedback.OnSuggestionDisplayed)) + if (predictor.Id == predictorId) { - callBack ??= GetCallBack(client, session, countOrIndex); - ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); + if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.SuggestionDisplayed)) + { + Action callBack = GetCallBack(client, session, countOrIndex); + ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); + } + + break; } } @@ -253,13 +257,17 @@ public static void OnSuggestionAccepted(PredictionClient client, Guid predictorI return; } - Action? callBack = null; foreach (ICommandPredictor predictor in predictors) { - if (predictor.Id == predictorId && predictor.AcceptFeedback(client, PredictorFeedback.OnSuggestionAccepted)) + if (predictor.Id == predictorId) { - callBack ??= GetCallBack(client, session, suggestionText); - ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); + if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.SuggestionAccepted)) + { + Action callBack = GetCallBack(client, session, suggestionText); + ThreadPool.QueueUserWorkItem(callBack, predictor, preferLocal: false); + } + + break; } } diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs index 6fe31747060..8ebdced993a 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs @@ -9,7 +9,7 @@ using System.Management.Automation.Language; using System.Threading; -namespace System.Management.Automation.Subsystem +namespace System.Management.Automation.Subsystem.Prediction { /// /// Interface for implementing a predictor plugin. @@ -26,14 +26,6 @@ public interface ICommandPredictor : ISubsystem /// SubsystemKind ISubsystem.Kind => SubsystemKind.CommandPredictor; - /// - /// Gets a value indicating whether the predictor accepts a specific feedback. - /// - /// Represents the client that initiates the call. - /// A specific type of feedback. - /// True or false, to indicate whether the specific feedback is accepted. - bool AcceptFeedback(PredictionClient client, PredictorFeedback feedback); - /// /// Get the predictive suggestions. It indicates the start of a suggestion rendering session. /// @@ -43,6 +35,14 @@ public interface ICommandPredictor : ISubsystem /// An instance of . SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken); + /// + /// Gets a value indicating whether the predictor accepts a specific kind of feedback. + /// + /// Represents the client that initiates the call. + /// A specific type of feedback. + /// True or false, to indicate whether the specific feedback is accepted. + bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback); + /// /// One or more suggestions provided by the predictor were displayed to the user. /// @@ -75,57 +75,57 @@ public interface ICommandPredictor : ISubsystem /// /// Represents the client that initiates the call. /// The last accepted command line. - /// Shows whether the execution succeeded or failed. - void OnCommandLineExecuted(PredictionClient client, string commandLine, bool status); + /// Shows whether the execution was successful. + void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success); } /// /// Kinds of feedback a predictor can choose to accept. /// - public enum PredictorFeedback + public enum PredictorFeedbackKind { /// /// Feedback when one or more suggestions are displayed to the user. /// - OnSuggestionDisplayed, + SuggestionDisplayed, /// /// Feedback when a suggestion is accepted by the user. /// - OnSuggestionAccepted, + SuggestionAccepted, /// /// Feedback when a command line is accepted by the user. /// - OnCommandLineAccepted, + CommandLineAccepted, /// /// Feedback when the accepted command line finishes its execution. /// - OnCommandLineExecuted, + CommandLineExecuted, } /// - /// The class represents a client that interacts with predictors. + /// Kinds of prediction clients. /// - public class PredictionClient + public enum PredictionClientKind { /// - /// Kinds of the client. + /// A terminal client, representing the command-line experience. /// - public enum ClientKind - { - /// - /// A terminal client, representing the command-line experience. - /// - Terminal, - - /// - /// An editor client, representing the editor experience. - /// - Editor, - } + Terminal, + + /// + /// An editor client, representing the editor experience. + /// + Editor, + } + /// + /// The class represents a client that interacts with predictors. + /// + public class PredictionClient + { /// /// Gets the client name. /// @@ -134,14 +134,14 @@ public enum ClientKind /// /// Gets the client kind. /// - public ClientKind Kind { get; } + public PredictionClientKind Kind { get; } /// /// Initializes a new instance of the class. /// /// Name of the interactive client. /// Kind of the interactive client. - public PredictionClient(string name, ClientKind kind) + public PredictionClient(string name, PredictionClientKind kind) { Name = name; Kind = kind; diff --git a/src/System.Management.Automation/engine/Subsystem/SubsystemManager.cs b/src/System.Management.Automation/engine/Subsystem/SubsystemManager.cs index 81a27f61279..1d90fc73343 100644 --- a/src/System.Management.Automation/engine/Subsystem/SubsystemManager.cs +++ b/src/System.Management.Automation/engine/Subsystem/SubsystemManager.cs @@ -8,6 +8,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Management.Automation.Internal; +using System.Management.Automation.Subsystem.Prediction; namespace System.Management.Automation.Subsystem { diff --git a/test/xUnit/csharp/test_Prediction.cs b/test/xUnit/csharp/test_Prediction.cs index 7975d2a96f3..938f892d8b5 100644 --- a/test/xUnit/csharp/test_Prediction.cs +++ b/test/xUnit/csharp/test_Prediction.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Management.Automation.Language; using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.Prediction; using System.Threading; using Xunit; @@ -70,7 +71,7 @@ public void Clear() public string Description => _description; - bool ICommandPredictor.AcceptFeedback(PredictionClient client, PredictorFeedback feedback) => true; + bool ICommandPredictor.CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback) => true; public SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken) { @@ -110,9 +111,9 @@ public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList } } - public void OnCommandLineExecuted(PredictionClient client, string commandLine, bool status) + public void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success) { - Results.Add($"{client.Name}-{commandLine}-{status}"); + Results.Add($"{client.Name}-{commandLine}-{success}"); } #endregion @@ -122,7 +123,7 @@ public static class CommandPredictionTests { private const string Client = "PredictionTest"; private const uint Session = 56; - private static PredictionClient predClient = new(Client, PredictionClient.ClientKind.Terminal); + private static PredictionClient predClient = new(Client, PredictionClientKind.Terminal); [Fact] public static void PredictInput() diff --git a/test/xUnit/csharp/test_Subsystem.cs b/test/xUnit/csharp/test_Subsystem.cs index f54daa45eeb..54fdcbf83bd 100644 --- a/test/xUnit/csharp/test_Subsystem.cs +++ b/test/xUnit/csharp/test_Subsystem.cs @@ -4,6 +4,7 @@ using System; using System.Collections.ObjectModel; using System.Management.Automation.Subsystem; +using System.Management.Automation.Subsystem.Prediction; using System.Threading; using Xunit; @@ -97,7 +98,7 @@ public static void RegisterSubsystem() const string Client = "SubsystemTest"; const string Input = "Hello world"; - var predClient = new PredictionClient(Client, PredictionClient.ClientKind.Terminal); + var predClient = new PredictionClient(Client, PredictionClientKind.Terminal); var predCxt = PredictionContext.Create(Input); var results = impl.GetSuggestion(predClient, predCxt, CancellationToken.None); Assert.Equal($"'{Input}' from '{Client}' - TEST-1 from {impl.Name}", results.SuggestionEntries[0].SuggestionText); From fb4a8e05dd9b10460180a2af8da25aa2053b806d Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 18 May 2021 21:16:19 -0700 Subject: [PATCH 7/8] Rename 'PredictInput' to 'PredictInputAsync' --- .../engine/Subsystem/CommandPrediction/CommandPrediction.cs | 6 +++--- test/xUnit/csharp/test_Prediction.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs index f9c91978d57..7a87b259314 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs @@ -59,9 +59,9 @@ public static class CommandPrediction /// The object from parsing the current command line input. /// The objects from parsing the current command line input. /// A list of objects. - public static Task?> PredictInput(PredictionClient client, Ast ast, Token[] astTokens) + public static Task?> PredictInputAsync(PredictionClient client, Ast ast, Token[] astTokens) { - return PredictInput(client, ast, astTokens, millisecondsTimeout: 20); + return PredictInputAsync(client, ast, astTokens, millisecondsTimeout: 20); } /// @@ -72,7 +72,7 @@ public static class CommandPrediction /// The objects from parsing the current command line input. /// The milliseconds to timeout. /// A list of objects. - public static async Task?> PredictInput(PredictionClient client, Ast ast, Token[] astTokens, int millisecondsTimeout) + public static async Task?> PredictInputAsync(PredictionClient client, Ast ast, Token[] astTokens, int millisecondsTimeout) { Requires.Condition(millisecondsTimeout > 0, nameof(millisecondsTimeout)); diff --git a/test/xUnit/csharp/test_Prediction.cs b/test/xUnit/csharp/test_Prediction.cs index 938f892d8b5..12e61dcd848 100644 --- a/test/xUnit/csharp/test_Prediction.cs +++ b/test/xUnit/csharp/test_Prediction.cs @@ -134,7 +134,7 @@ public static void PredictInput() Ast ast = Parser.ParseInput(Input, out Token[] tokens, out _); // Returns null when no predictor implementation registered - List results = CommandPrediction.PredictInput(predClient, ast, tokens).Result; + List results = CommandPrediction.PredictInputAsync(predClient, ast, tokens).Result; Assert.Null(results); try @@ -147,7 +147,7 @@ public static void PredictInput() // cannot finish before the specified timeout. // The specified timeout is exaggerated to make the test reliable. // xUnit must spin up a lot tasks, which makes the test unreliable when the time difference between 'delay' and 'timeout' is small. - results = CommandPrediction.PredictInput(predClient, ast, tokens, millisecondsTimeout: 1000).Result; + results = CommandPrediction.PredictInputAsync(predClient, ast, tokens, millisecondsTimeout: 1000).Result; Assert.Single(results); PredictionResult res = results[0]; @@ -160,7 +160,7 @@ public static void PredictInput() // Expect the results from both 'slow' and 'fast' predictors // Same here -- the specified timeout is exaggerated to make the test reliable. // xUnit must spin up a lot tasks, which makes the test unreliable when the time difference between 'delay' and 'timeout' is small. - results = CommandPrediction.PredictInput(predClient, ast, tokens, millisecondsTimeout: 4000).Result; + results = CommandPrediction.PredictInputAsync(predClient, ast, tokens, millisecondsTimeout: 4000).Result; Assert.Equal(2, results.Count); PredictionResult res1 = results[0]; From 7b02708b697c45dd99e94b591bf201897d1fc361 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 19 May 2021 11:43:14 -0700 Subject: [PATCH 8/8] Address Ilya's feedback --- .../engine/Subsystem/CommandPrediction/ICommandPredictor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs index 8ebdced993a..bd8a48a85db 100644 --- a/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs +++ b/src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs @@ -124,7 +124,7 @@ public enum PredictionClientKind /// /// The class represents a client that interacts with predictors. /// - public class PredictionClient + public sealed class PredictionClient { /// /// Gets the client name. @@ -151,7 +151,7 @@ public PredictionClient(string name, PredictionClientKind kind) /// /// Context information about the user input. /// - public class PredictionContext + public sealed class PredictionContext { /// /// Gets the abstract syntax tree (AST) generated from parsing the user input.