Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,22 @@ public sealed class PredictionResult
/// </summary>
public string Name { get; }

/// <summary>
/// Gets the mini-session id that represents a specific invocation to the <see cref="ICommandPredictor.GetSuggestion"/> API of the predictor.
/// When it's not specified, it's considered by a client that the predictor doesn't expect feedback.
/// </summary>
public uint? Session { get; }

/// <summary>
/// Gets the suggestions.
/// </summary>
public IReadOnlyList<PredictiveSuggestion> Suggestions { get; }

internal PredictionResult(Guid id, string name, List<PredictiveSuggestion> suggestions)
internal PredictionResult(Guid id, string name, uint? session, List<PredictiveSuggestion> suggestions)
{
Id = id;
Name = name;
Session = session;
Suggestions = suggestions;
}
}
Expand All @@ -48,22 +55,24 @@ public static class CommandPrediction
/// <summary>
/// Collect the predictive suggestions from registered predictors using the default timeout.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <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(Ast ast, Token[] astTokens)
public static Task<List<PredictionResult>?> PredictInput(string client, Ast ast, Token[] astTokens)
{
return PredictInput(ast, astTokens, millisecondsTimeout: 20);
return PredictInput(client, ast, astTokens, millisecondsTimeout: 20);
}

/// <summary>
/// Collect the predictive suggestions from registered predictors using the specified timeout.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <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>
/// <param name="millisecondsTimeout">The milliseconds to timeout.</param>
/// <returns>A list of <see cref="PredictionResult"/> objects.</returns>
public static async Task<List<PredictionResult>?> PredictInput(Ast ast, Token[] astTokens, int millisecondsTimeout)
public static async Task<List<PredictionResult>?> PredictInput(string client, Ast ast, Token[] astTokens, int millisecondsTimeout)
{
Requires.Condition(millisecondsTimeout > 0, nameof(millisecondsTimeout));

Expand All @@ -85,8 +94,8 @@ public static class CommandPrediction
state =>
{
var predictor = (ICommandPredictor)state!;
List<PredictiveSuggestion>? texts = predictor.GetSuggestion(context, cancellationSource.Token);
return texts?.Count > 0 ? new PredictionResult(predictor.Id, predictor.Name, texts) : null;
SuggestionPackage pkg = predictor.GetSuggestion(client, context, cancellationSource.Token);
return pkg.SuggestionEntries?.Count > 0 ? new PredictionResult(predictor.Id, predictor.Name, pkg.Session, pkg.SuggestionEntries) : null;
},
predictor,
cancellationSource.Token,
Expand All @@ -99,27 +108,28 @@ await Task.WhenAny(
Task.Delay(millisecondsTimeout, cancellationSource.Token)).ConfigureAwait(false);
cancellationSource.Cancel();

var results = new List<PredictionResult>(predictors.Count);
var resultList = new List<PredictionResult>(predictors.Count);
foreach (Task<PredictionResult?> task in tasks)
{
if (task.IsCompletedSuccessfully)
{
PredictionResult? result = task.Result;
if (result != null)
{
results.Add(result);
resultList.Add(result);
}
}
}

return results;
return resultList;
}

/// <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(IReadOnlyList<string> history)
public static void OnCommandLineAccepted(string client, IReadOnlyList<string> history)
{
Requires.NotNull(history, nameof(history));

Expand All @@ -134,19 +144,51 @@ public static void OnCommandLineAccepted(IReadOnlyList<string> history)
if (predictor.SupportEarlyProcessing)
{
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
state => state.StartEarlyProcessing(history),
state => state.StartEarlyProcessing(client, history),
predictor,
preferLocal: false);
}
}
}

/// <summary>
/// Send feedback to a predictor when one or more suggestions from it were displayed to the user.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="predictorId">The identifier of the predictor whose prediction result was accepted.</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>
public static void OnSuggestionDisplayed(string client, Guid predictorId, uint session, int countOrIndex)
{
var predictors = SubsystemManager.GetSubsystems<ICommandPredictor>();
if (predictors.Count == 0)
{
return;
}

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

/// <summary>
/// Send feedback to predictors about their last suggestions.
/// Send feedback to a predictor when a suggestion from it was accepted.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <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(Guid predictorId, string suggestionText)
public static void OnSuggestionAccepted(string client, Guid predictorId, uint session, string suggestionText)
{
Requires.NotNullOrEmpty(suggestionText, nameof(suggestionText));

Expand All @@ -161,7 +203,7 @@ public static void OnSuggestionAccepted(Guid predictorId, string suggestionText)
if (predictor.AcceptFeedback && predictor.Id == predictorId)
{
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
state => state.OnSuggestionAccepted(suggestionText),
state => state.OnSuggestionAccepted(client, session, suggestionText),
predictor,
preferLocal: false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,37 @@ public interface ICommandPredictor : ISubsystem
/// 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(IReadOnlyList<string> history);
void StartEarlyProcessing(string clientId, IReadOnlyList<string> history);

/// <summary>
/// The suggestion given by the predictor was accepted.
/// Get the predictive suggestions. It indicates the start of a suggestion rendering session.
/// </summary>
/// <param name="acceptedSuggestion">The accepted suggestion text.</param>
void OnSuggestionAccepted(string acceptedSuggestion);
/// <param name="clientId">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);

/// <summary>
/// Get the predictive suggestions.
/// One or more suggestions provided by the predictor were displayed to the user.
/// </summary>
/// <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>A list of predictive suggestions.</returns>
List<PredictiveSuggestion>? GetSuggestion(PredictionContext context, CancellationToken cancellationToken);
/// <param name="clientId">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);

/// <summary>
/// The suggestion provided by the predictor was accepted.
/// </summary>
/// <param name="clientId">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);
}

/// <summary>
Expand Down Expand Up @@ -160,4 +175,47 @@ public PredictiveSuggestion(string suggestion, string? toolTip)
ToolTip = toolTip;
}
}

/// <summary>
/// A package returned from <see cref="ICommandPredictor.GetSuggestion"/>.
/// </summary>
public struct SuggestionPackage
{
/// <summary>
/// Gets the mini-session that represents a specific invocation to <see cref="ICommandPredictor.GetSuggestion"/>.
/// When it's not specified, it's considered by a client that the predictor doesn't expect feedback.
/// </summary>
public uint? Session { get; }

/// <summary>
/// Gets the suggestion entries returned from that mini-session.
/// </summary>
public List<PredictiveSuggestion>? SuggestionEntries { get; }

/// <summary>
/// Initializes a new instance of the <see cref="SuggestionPackage"/> struct without providing a session id.
/// Note that, when a session id is not specified, it's considered by a client that the predictor doesn't expect feedback.
/// </summary>
/// <param name="suggestionEntries">The suggestions to return.</param>
public SuggestionPackage(List<PredictiveSuggestion> suggestionEntries)
{
Requires.NotNullOrEmpty(suggestionEntries, nameof(suggestionEntries));

Session = null;
SuggestionEntries = suggestionEntries;
}

/// <summary>
/// Initializes a new instance of the <see cref="SuggestionPackage"/> struct with the mini-session id and the suggestions.
/// </summary>
/// <param name="session">The mini-session where suggestions came from.</param>
/// <param name="suggestionEntries">The suggestions to return.</param>
public SuggestionPackage(uint session, List<PredictiveSuggestion> suggestionEntries)
{
Requires.NotNullOrEmpty(suggestionEntries, nameof(suggestionEntries));

Session = session;
SuggestionEntries = suggestionEntries;
}
}
}
8 changes: 8 additions & 0 deletions src/System.Management.Automation/engine/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2312,6 +2312,14 @@ internal static void NotNullOrEmpty(string value, string paramName)
}
}

internal static void NotNullOrEmpty(ICollection value, string paramName)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps use [CallerArgumentExpression("value")] ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not implemented yet, right? See the proposal doc and the open issue: dotnet/csharplang#287

Copy link
Collaborator

@powercode powercode Feb 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct! I was misled by the docs. I have used CallerMemberName and read the documentation for CAE and just assumed it worked as well.

{
if (value == null || value.Count == 0)
{
throw new ArgumentNullException(paramName);
}
}

internal static void Condition([DoesNotReturnIf(false)] bool precondition, string paramName)
{
if (!precondition)
Expand Down
Loading