Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
using System.Threading;
using System.Threading.Tasks;

namespace System.Management.Automation.Subsystem
namespace System.Management.Automation.Subsystem.Prediction
{
/// <summary>
/// The class represents the prediction result from a predictor.
Expand Down Expand Up @@ -59,9 +59,9 @@ public static class CommandPrediction
/// <param name="ast">The <see cref="Ast"/> object from parsing the current command line input.</param>
/// <param name="astTokens">The <see cref="Token"/> objects from parsing the current command line input.</param>
/// <returns>A list of <see cref="PredictionResult"/> objects.</returns>
public static Task<List<PredictionResult>?> PredictInput(string client, Ast ast, Token[] astTokens)
public static Task<List<PredictionResult>?> PredictInputAsync(PredictionClient client, Ast ast, Token[] astTokens)
Comment thread
daxian-dbw marked this conversation as resolved.
{
return PredictInput(client, ast, astTokens, millisecondsTimeout: 20);
return PredictInputAsync(client, ast, astTokens, millisecondsTimeout: 20);
}

/// <summary>
Expand All @@ -72,7 +72,7 @@ public static class CommandPrediction
/// <param name="astTokens">The <see cref="Token"/> objects from parsing the current command line input.</param>
/// <param name="millisecondsTimeout">The milliseconds to timeout.</param>
/// <returns>A list of <see cref="PredictionResult"/> objects.</returns>
public static async Task<List<PredictionResult>?> PredictInput(string client, Ast ast, Token[] astTokens, int millisecondsTimeout)
public static async Task<List<PredictionResult>?> PredictInputAsync(PredictionClient client, Ast ast, Token[] astTokens, int millisecondsTimeout)
{
Requires.Condition(millisecondsTimeout > 0, nameof(millisecondsTimeout));

Expand All @@ -86,17 +86,13 @@ public static class CommandPrediction
var tasks = new Task<PredictionResult?>[predictors.Count];
using var cancellationSource = new CancellationTokenSource();

Func<object?, PredictionResult?> 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,
Expand All @@ -122,14 +118,29 @@ 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<object?, PredictionResult?> 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;
};
}
}

/// <summary>
/// Allow registered predictors to do early processing when a command line is accepted.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="history">History command lines provided as references for prediction.</param>
public static void OnCommandLineAccepted(string client, IReadOnlyList<string> history)
public static void OnCommandLineAccepted(PredictionClient client, IReadOnlyList<string> history)
{
Requires.NotNull(history, nameof(history));

Expand All @@ -139,16 +150,54 @@ public static void OnCommandLineAccepted(string client, IReadOnlyList<string> hi
return;
}

Action<ICommandPredictor>? callBack = null;
foreach (ICommandPredictor predictor in predictors)
{
if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.CommandLineAccepted))
{
callBack ??= GetCallBack(client, history);
ThreadPool.QueueUserWorkItem<ICommandPredictor>(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<ICommandPredictor> GetCallBack(PredictionClient client, IReadOnlyList<string> history)
{
return predictor => predictor.OnCommandLineAccepted(client, history);
}
}

/// <summary>
/// Allow registered predictors to know the execution result (success/failure) of the last accepted command line.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="commandLine">The last accepted command line.</param>
/// <param name="success">Whether the execution of the last command line was successful.</param>
public static void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success)
{
var predictors = SubsystemManager.GetSubsystems<ICommandPredictor>();
Comment thread
daxian-dbw marked this conversation as resolved.
if (predictors.Count == 0)
{
return;
}

Action<ICommandPredictor>? callBack = null;
foreach (ICommandPredictor predictor in predictors)
{
if (predictor.SupportEarlyProcessing)
if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.CommandLineExecuted))
{
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
state => state.StartEarlyProcessing(client, history),
predictor,
preferLocal: false);
callBack ??= GetCallBack(client, commandLine, success);
ThreadPool.QueueUserWorkItem<ICommandPredictor>(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<ICommandPredictor> GetCallBack(PredictionClient client, string commandLine, bool success)
{
return predictor => predictor.OnCommandLineExecuted(client, commandLine, success);
}
}

/// <summary>
Expand All @@ -161,7 +210,7 @@ public static void OnCommandLineAccepted(string client, IReadOnlyList<string> hi
/// When the value is greater than 0, it's the number of displayed suggestions from the list returned in <paramref name="session"/>, 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.
/// </param>
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<ICommandPredictor>();
if (predictors.Count == 0)
Expand All @@ -171,14 +220,24 @@ public static void OnSuggestionDisplayed(string client, Guid predictorId, uint s

foreach (ICommandPredictor predictor in predictors)
{
if (predictor.AcceptFeedback && predictor.Id == predictorId)
if (predictor.Id == predictorId)
{
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
state => state.OnSuggestionDisplayed(client, session, countOrIndex),
predictor,
preferLocal: false);
if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.SuggestionDisplayed))
{
Action<ICommandPredictor> callBack = GetCallBack(client, session, countOrIndex);
ThreadPool.QueueUserWorkItem<ICommandPredictor>(callBack, predictor, preferLocal: false);
}

break;
}
}

// 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<ICommandPredictor> GetCallBack(PredictionClient client, uint session, int countOrIndex)
{
return predictor => predictor.OnSuggestionDisplayed(client, session, countOrIndex);
}
}

/// <summary>
Expand All @@ -188,7 +247,7 @@ public static void OnSuggestionDisplayed(string client, Guid predictorId, uint s
/// <param name="predictorId">The identifier of the predictor whose prediction result was accepted.</param>
/// <param name="session">The mini-session where the accepted suggestion came from.</param>
/// <param name="suggestionText">The accepted suggestion text.</param>
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));

Expand All @@ -200,14 +259,24 @@ public static void OnSuggestionAccepted(string client, Guid predictorId, uint se

foreach (ICommandPredictor predictor in predictors)
{
if (predictor.AcceptFeedback && predictor.Id == predictorId)
if (predictor.Id == predictorId)
{
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
state => state.OnSuggestionAccepted(client, session, suggestionText),
predictor,
preferLocal: false);
if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.SuggestionAccepted))
{
Action<ICommandPredictor> callBack = GetCallBack(client, session, suggestionText);
ThreadPool.QueueUserWorkItem<ICommandPredictor>(callBack, predictor, preferLocal: false);
}

break;
}
}

// 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<ICommandPredictor> GetCallBack(PredictionClient client, uint session, string suggestionText)
{
return predictor => predictor.OnSuggestionAccepted(client, session, suggestionText);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
using System.Management.Automation.Language;
using System.Threading;

namespace System.Management.Automation.Subsystem
namespace System.Management.Automation.Subsystem.Prediction
{
/// <summary>
/// Interface for implementing a predictor plugin.
Expand All @@ -26,57 +26,132 @@ public interface ICommandPredictor : ISubsystem
/// </summary>
SubsystemKind ISubsystem.Kind => SubsystemKind.CommandPredictor;

/// <summary>
/// Gets a value indicating whether the predictor supports early processing.
/// </summary>
bool SupportEarlyProcessing { get; }

/// <summary>
/// Gets a value indicating whether the predictor accepts feedback about the previous suggestion.
/// </summary>
bool AcceptFeedback { get; }

/// <summary>
/// A command line was accepted to execute.
/// The predictor can start processing early as needed with the latest history.
/// </summary>
/// <param name="clientId">Represents the client that initiates the call.</param>
/// <param name="history">History command lines provided as references for prediction.</param>
void StartEarlyProcessing(string clientId, IReadOnlyList<string> history);

/// <summary>
/// Get the predictive suggestions. It indicates the start of a suggestion rendering session.
/// </summary>
/// <param name="clientId">Represents the client that initiates the call.</param>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="context">The <see cref="PredictionContext"/> object to be used for prediction.</param>
/// <param name="cancellationToken">The cancellation token to cancel the prediction.</param>
/// <returns>An instance of <see cref="SuggestionPackage"/>.</returns>
SuggestionPackage GetSuggestion(string clientId, PredictionContext context, CancellationToken cancellationToken);
SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken);

/// <summary>
/// Gets a value indicating whether the predictor accepts a specific kind of feedback.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="feedback">A specific type of feedback.</param>
/// <returns>True or false, to indicate whether the specific feedback is accepted.</returns>
bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback);

/// <summary>
/// One or more suggestions provided by the predictor were displayed to the user.
/// </summary>
/// <param name="clientId">Represents the client that initiates the call.</param>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="session">The mini-session where the displayed suggestions came from.</param>
/// <param name="countOrIndex">
/// When the value is greater than 0, it's the number of displayed suggestions from the list returned in <paramref name="session"/>, 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.
/// </param>
void OnSuggestionDisplayed(string clientId, uint session, int countOrIndex);
void OnSuggestionDisplayed(PredictionClient client, uint session, int countOrIndex);

/// <summary>
/// The suggestion provided by the predictor was accepted.
/// </summary>
/// <param name="clientId">Represents the client that initiates the call.</param>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="session">Represents the mini-session where the accepted suggestion came from.</param>
/// <param name="acceptedSuggestion">The accepted suggestion text.</param>
void OnSuggestionAccepted(string clientId, uint session, string acceptedSuggestion);
void OnSuggestionAccepted(PredictionClient client, uint session, string acceptedSuggestion);

/// <summary>
/// A command line was accepted to execute.
/// The predictor can start processing early as needed with the latest history.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="history">History command lines provided as references for prediction.</param>
void OnCommandLineAccepted(PredictionClient client, IReadOnlyList<string> history);

/// <summary>
/// A command line was done execution.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="commandLine">The last accepted command line.</param>
/// <param name="success">Shows whether the execution was successful.</param>
void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success);
}

/// <summary>
/// Kinds of feedback a predictor can choose to accept.
Comment thread
daxian-dbw marked this conversation as resolved.
/// </summary>
public enum PredictorFeedbackKind
{
/// <summary>
/// Feedback when one or more suggestions are displayed to the user.
/// </summary>
SuggestionDisplayed,

/// <summary>
/// Feedback when a suggestion is accepted by the user.
/// </summary>
SuggestionAccepted,

/// <summary>
/// Feedback when a command line is accepted by the user.
/// </summary>
CommandLineAccepted,

/// <summary>
/// Feedback when the accepted command line finishes its execution.
/// </summary>
CommandLineExecuted,
}

/// <summary>
/// Kinds of prediction clients.
/// </summary>
public enum PredictionClientKind
{
/// <summary>
/// A terminal client, representing the command-line experience.
/// </summary>
Terminal,

/// <summary>
/// An editor client, representing the editor experience.
/// </summary>
Editor,
}

/// <summary>
/// The class represents a client that interacts with predictors.
/// </summary>
public sealed class PredictionClient
{
/// <summary>
/// Gets the client name.
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the client kind.
/// </summary>
public PredictionClientKind Kind { get; }

/// <summary>
/// Initializes a new instance of the <see cref="PredictionClient"/> class.
/// </summary>
/// <param name="name">Name of the interactive client.</param>
/// <param name="kind">Kind of the interactive client.</param>
public PredictionClient(string name, PredictionClientKind kind)
{
Name = name;
Kind = kind;
}
}

/// <summary>
/// Context information about the user input.
/// </summary>
public class PredictionContext
public sealed class PredictionContext
{
/// <summary>
/// Gets the abstract syntax tree (AST) generated from parsing the user input.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Loading