8000 Update prediction interface to provide additional feedback to a predi… · PowerShell/PowerShell@5febcad · GitHub
[go: up one dir, main page]

Skip to content

Commit 5febcad

Browse files
authored
Update prediction interface to provide additional feedback to a predictor plugin (#15421)
1 parent e927e94 commit 5febcad

File tree

5 files changed

+271
-92
lines changed

5 files changed

+271
-92
lines changed

src/System.Management.Automation/engine/Subsystem/CommandPrediction/CommandPrediction.cs

Lines changed: 98 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
using System.Threading;
1111
using System.Threading.Tasks;
1212

13-
namespace System.Management.Automation.Subsystem
13+
namespace System.Management.Automation.Subsystem.Prediction
1414
{
1515
/// <summary>
1616
/// The class represents the prediction result from a predictor.
@@ -59,9 +59,9 @@ public static class CommandPrediction
5959
/// <param name="ast">The <see cref="Ast"/> object from parsing the current command line input.</param>
6060
/// <param name="astTokens">The <see cref="Token"/> objects from parsing the current command line input.</param>
6161
/// <returns>A list of <see cref="PredictionResult"/> objects.</returns>
62-
public static Task<List<PredictionResult>?> PredictInput(string client, Ast ast, Token[] astTokens)
62+
public static Task<List<PredictionResult>?> PredictInputAsync(PredictionClient client, Ast ast, Token[] astTokens)
6363
{
64-
return PredictInput(client, ast, astTokens, millisecondsTimeout: 20);
64+
return PredictInputAsync(client, ast, astTokens, millisecondsTimeout: 20);
6565
}
6666

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

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

89+
Func<object?, PredictionResult?> callBack = GetCallBack(client, context, cancellationSource);
90+
8991
for (int i = 0; i < predictors.Count; i++)
9092
{
9193
ICommandPredictor predictor = predictors[i];
92-
9394
tasks[i] = Task.Factory.StartNew(
94-
state =>
95-
{
96-
var predictor = (ICommandPredictor)state!;
97-
SuggestionPackage pkg = predictor.GetSuggestion(client, context, cancellationSource.Token);
98-
return pkg.SuggestionEntries?.Count > 0 ? new PredictionResult(predictor.Id, predictor.Name, pkg.Session, pkg.SuggestionEntries) : null;
99-
},
95+
callBack,
10096
predictor,
10197
cancellationSource.Token,
10298
TaskCreationOptions.DenyChildAttach,
@@ -122,14 +118,29 @@ await Task.WhenAny(
122118
}
123119

124120
return resultList;
121+
122+
// A local helper function to avoid creating an instance of the generated delegate helper class
123+
// when no predictor is registered.
124+
static Func<object?, PredictionResult?> GetCallBack(
125+
PredictionClient client,
126+
PredictionContext context,
127+
CancellationTokenSource cancellationSource)
128+
{
129+
return state =>
130+
{
131+
var predictor = (ICommandPredictor)state!;
132+
SuggestionPackage pkg = predictor.GetSuggestion(client, context, cancellationSource.Token);
133+
return pkg.SuggestionEntries?.Count > 0 ? new PredictionResult(predictor.Id, predictor.Name, pkg.Session, pkg.SuggestionEntries) : null;
134+
};
135+
}
125136
}
126137

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

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

153+
Action<ICommandPredictor>? callBack = null;
154+
foreach (ICommandPredictor predictor in predictors)
155+
{
156+
if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.CommandLineAccepted))
157+
{
158+
callBack ??= GetCallBack(client, history);
159+
ThreadPool.QueueUserWorkItem<ICommandPredictor>(callBack, predictor, preferLocal: false);
< FAB2 code>160+
}
161+
}
162+
163+
// A local helper function to avoid creating an instance of the generated delegate helper class
164+
// when no predictor is registered, or no registered predictor accepts this feedback.
165+
static Action<ICommandPredictor> GetCallBack(PredictionClient client, IReadOnlyList<string> history)
166+
{
167+
return predictor => predictor.OnCommandLineAccepted(client, history);
168+
}
169+
}
170+
171+
/// <summary>
172+
/// Allow registered predictors to know the execution result (success/failure) of the last accepted command line.
173+
/// </summary>
174+
/// <param name="client">Represents the client that initiates the call.</param>
175+
/// <param name="commandLine">The last accepted command line.</param>
176+
/// <param name="success">Whether the execution of the last command line was successful.</param>
177+
public static void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success)
178+
{
179+
var predictors = SubsystemManager.GetSubsystems<ICommandPredictor>();
180+
if (predictors.Count == 0)
181+
{
182+
return;
183+
}
184+
185+
Action<ICommandPredictor>? callBack = null;
142186
foreach (ICommandPredictor predictor in predictors)
143187
{
144-
if (predictor.SupportEarlyProcessing)
188+
if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.CommandLineExecuted))
145189
{
146-
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
147-
state => state.StartEarlyProcessing(client, history),
148-
predictor,
149-
preferLocal: false);
190+
callBack ??= GetCallBack(client, commandLine, success);
191+
ThreadPool.QueueUserWorkItem<ICommandPredictor>(callBack, predictor, preferLocal: false);
150192
}
151193
}
194+
195+
// A local helper function to avoid creating an instance of the generated delegate helper class
196+
// when no predictor is registered, or no registered predictor accepts this feedback.
197+
static Action<ICommandPredictor> GetCallBack(PredictionClient client, string commandLine, bool success)
198+
{
199+
return predictor => predictor.OnCommandLineExecuted( F438 client, commandLine, success);
200+
}
152201
}
153202

154203
/// <summary>
@@ -161,7 +210,7 @@ public static void OnCommandLineAccepted(string client, IReadOnlyList<string> hi
161210
/// 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.
162211
/// 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.
163212
/// </param>
164-
public static void OnSuggestionDisplayed(string client, Guid predictorId, uint session, int countOrIndex)
213+
public static void OnSuggestionDisplayed(PredictionClient client, Guid predictorId, uint session, int countOrIndex)
165214
{
166215
var predictors = SubsystemManager.GetSubsystems<ICommandPredictor>();
167216
if (predictors.Count == 0)
@@ -171,14 +220,24 @@ public static void OnSuggestionDisplayed(string client, Guid predictorId, uint s
171220

172221
foreach (ICommandPredictor predictor in predictors)
173222
{
174-
if (predictor.AcceptFeedback && predictor.Id == predictorId)
223+
if (predictor.Id == predictorId)
175224
{
176-
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
177-
state => state.OnSuggestionDisplayed(client, session, countOrIndex),
178-
predictor,
179-
preferLocal: false);
225+
if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.SuggestionDisplayed))
226+
{
227+
Action<ICommandPredictor> callBack = GetCallBack(client, session, countOrIndex);
228+
ThreadPool.QueueUserWorkItem<ICommandPredictor>(callBack, predictor, preferLocal: false);
229+
}
230+
231+
break;
180232
}
181233
}
234+
235+
// A local helper function to avoid creating an instance of the generated delegate helper class
236+
// when no predictor is registered, or no registered predictor accepts this feedback.
237+
static Action<ICommandPredictor> GetCallBack(PredictionClient client, uint session, int countOrIndex)
238+
{
239+
return predictor => predictor.OnSuggestionDisplayed(client, session, countOrIndex);
240+
}
182241
}
183242

184243
/// <summary>
@@ -188,7 +247,7 @@ public static void OnSuggestionDisplayed(string client, Guid predictorId, uint s
188247
/// <param name="predictorId">The identifier of the predictor whose prediction result was accepted.</param>
189248
/// <param name="session">The mini-session where the accepted suggestion came from.</param>
190249
/// <param name="suggestionText">The accepted suggestion text.</param>
191-
public static void OnSuggestionAccepted(string client, Guid predictorId, uint session, string suggestionText)
250+
public static void OnSuggestionAccepted(PredictionClient client, Guid predictorId, uint session, string suggestionText)
192251
{
193252
Requires.NotNullOrEmpty(suggestionText, nameof(suggestionText));
194253

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

201260
foreach (ICommandPredictor predictor in predictors)
202261
{
203-
if (predictor.AcceptFeedback && predictor.Id == predictorId)
262+
if (predictor.Id == predictorId)
204263
{
205-
ThreadPool.QueueUserWorkItem<ICommandPredictor>(
206-
state => state.OnSuggestionAccepted(client, session, suggestionText),
207-
predictor,
208-
preferLocal: false);
264+
if (predictor.CanAcceptFeedback(client, PredictorFeedbackKind.SuggestionAccepted))
265+
{
266+
Action<ICommandPredictor> callBack = GetCallBack(client, session, suggestionText);
267+
ThreadPool.QueueUserWorkItem<ICommandPredictor>(callBack, predictor, preferLocal: false);
268+
}
269+
270+
break;
209271
}
210272
}
273+
274+
// A local helper function to avoid creating an instance of the generated delegate helper class
275+
// when no predictor is registered, or no registered predictor accepts this feedback.
276+
static Action<ICommandPredictor> GetCallBack(PredictionClient client, uint session, string suggestionText)
277+
{
278+
return predictor => predictor.OnSuggestionAccepted(client, session, suggestionText);
279+
}
211280
}
212281
}
213282
}

src/System.Management.Automation/engine/Subsystem/CommandPrediction/ICommandPredictor.cs

Lines changed: 101 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
using System.Management.Automation.Language;
1010
using System.Threading;
1111

12-
namespace System.Management.Automation.Subsystem
12+
namespace System.Management.Automation.Subsystem.Prediction
1313
{
1414
/// <summary>
1515
/// Interface for implementing a predictor plugin.
@@ -26,57 +26,132 @@ public interface ICommandPredictor : ISubsystem
2626
/// </summary>
2727
SubsystemKind ISubsystem.Kind => SubsystemKind.CommandPredictor;
2828

29-
/// <summary>
30-
/// Gets a value indicating whether the predictor supports early processing.
31-
/// </summary>
32-
bool SupportEarlyProcessing { get; }
33-
34-
/// <summary>
35-
/// Gets a value indicating whether the predictor accepts feedback about the previous suggestion.
36-
/// </summary>
37-
bool AcceptFeedback { get; }
38-
39-
/// <summary>
40-
/// A command line was accepted to execute.
41-
/// The predictor can start processing early as needed with the latest history.
42-
/// </summary>
43-
/// <param name="clientId">Represents the client that initiates the call.</param>
44-
/// <param name="history">History command lines provided as references for prediction.</param>
45-
void StartEarlyProcessing(string clientId, IReadOnlyList<string> history);
46-
4729
/// <summary>
4830
/// Get the predictive suggestions. It indicates the start of a suggestion rendering session.
4931
/// </summary>
50-
/// <param name="clientId">Represents the client that initiates the call.</param>
32+
/// <param name="client">Represents the client that initiates the call.</param>
5133
/// <param name="context">The <see cref="PredictionContext"/> object to be used for prediction.</param>
5234
/// <param name="cancellationToken">The cancellation token to cancel the prediction.</param>
5335
/// <returns>An instance of <see cref="SuggestionPackage"/>.</returns>
54-
SuggestionPackage GetSuggestion(string clientId, PredictionContext context, CancellationToken cancellationToken);
36+
SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken);
37+
38+
/// <summary>
39+
/// Gets a value indicating whether the predictor accepts a specific kind of feedback.
40+
/// </summary>
41+
/// <param name="client">Represents the client that initiates the call.</param>
42+
/// <param name="feedback">A specific type of feedback.</param>
43+
/// <returns>True or false, to indicate whether the specific feedback is accepted.</returns>
44+
bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback);
5545

5646
/// <summary>
5747
/// One or more suggestions provided by the predictor were displayed to the user.
5848
/// </summary>
59-
/// <param name="clientId">Represents the client that initiates the call.</param>
49+
/// <param name="client">Represents the client that initiates the call.</param>
6050
/// <param name="session">The mini-session where the displayed suggestions came from.</param>
6151
/// <param name="countOrIndex">
6252
/// 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.
6353
/// 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.
6454
/// </param>
65-
void OnSuggestionDisplayed(string clientId, uint session, int countOrIndex);
55+
void OnSuggestionDisplayed(PredictionClient client, uint session, int countOrIndex);
6656

6757
/// <summary>
6858
/// The suggestion provided by the predictor was accepted.
6959
/// </summary>
70-
/// <param name="clientId">Represents the client that initiates the call.</param>
60+
/// <param name="client">Represents the client that initiates the call.</param>
7161
/// <param name="session">Represents the mini-session where the accepted suggestion came from.</param>
7262
/// <param name="acceptedSuggestion">The accepted suggestion text.</param>
73-
void OnSuggestionAccepted(string clientId, uint session, string acceptedSuggestion);
63+
void OnSuggestionAccepted(PredictionClient client, uint session, string acceptedSuggestion);
64+
65+
/// <summary>
66+
/// A command line was accepted to execute.
67+
/// The predictor can start processing early as needed with the latest history.
68+
/// </summary>
69+
/// <param name="client">Represents the client that initiates the call.</param>
70+
/// <param name="history">History command lines provided as references for prediction.</param>
71+
void OnCommandLineAccepted(PredictionClient client, IReadOnlyList<string> history);
72+
73+
/// <summary>
74+
/// A command line was done execution.
75+
/// </summary>
76+
/// <param name="client">Represents the client that initiates the call.</param>
77+
/// <param name="commandLine">The last accepted command line.</param>
78+
/// <param name="success">Shows whether the execution was successful.</param>
79+
void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success);
80+
}
81+
82+
/// <summary>
83+
/// Kinds of feedback a predictor can choose to accept.
84+
/// </summary>
85+
public enum PredictorFeedbackKind
86+
{
87+
/// <summary>
88+
/// Feedback when one or more suggestions are displayed to the user.
89+
/// </summary>
90+
SuggestionDisplayed,
91+
92+
/// <summary>
93+
/// Feedback when a suggestion is accepted by the user.
94+
/// </summary>
95+
SuggestionAccepted,
96+
97+
/// <summary>
98+
/// Feedback when a command line is accepted by the user.
99+
/// </summary>
100+
CommandLineAccepted,
101+
102+
/// <summary>
103+
/// Feedback when the accepted command line finishes its execution.
104+
/// </summary>
105+
CommandLineExecuted,
106+
}
107+
108+
/// <summary>
109+
/// Kinds of prediction clients.
110+
/// </summary>
111+
public enum PredictionClientKind
112+
{
113+
/// <summary>
114+
/// A terminal client, representing the command-line experience.
115+
/// </summary>
116+
Terminal,
117+
118+
/// <summary>
119+
/// An editor client, representing the editor experience.
120+
/// </summary>
121+
Editor,
122+
}
123+
124+
/// <summary>
125+
/// The class represents a client that interacts with predictors.
126+
/// </summary>
127+
public sealed class PredictionClient
128+
{
129+
/// <summary>
130+
/// Gets the client name.
131+
/// </summary>
132+
public string Name { get; }
133+
134+
/// <summary>
135+
/// Gets the client kind.
136+
/// </summary>
137+
public PredictionClientKind Kind { get; }
138+
139+
/// <summary>
140+
/// Initializes a new instance of the <see cref="PredictionClient"/> class.
141+
/// </summary>
142+
/// <param name="name">Name of the interactive client.</param>
143+
/// <param name="kind">Kind of the interactive client.</param>
144+
public PredictionClient(string name, PredictionClientKind kind)
145+
{
146+
Name = name;
147+
Kind = kind;
148+
}
74149
}
75150

76151
/// <summary>
77152
/// Context information about the user input.
78153
/// </summary>
79-
public class PredictionContext
154+
public sealed class PredictionContext
80155
{
81156
/// <summary>
82157
/// Gets the abstract syntax tree (AST) generated from parsing the user input.

src/System.Management.Automation/engine/Subsystem/SubsystemManager.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Collections.ObjectModel;
99
using System.Linq;
1010
using System.Management.Automation.Internal;
11+
using System.Management.Automation.Subsystem.Prediction;
1112

1213
namespace System.Management.Automation.Subsystem
1314
{

0 commit comments

Comments
 (0)
0