Skip to content

Commit 6ce82f2

Browse files
committed
Add PSHostUserInterface support for IHostUISupportsMultipleChoiceSelection
This change adds support for multi-select choice prompts to our PSHostUserInterface implementation by implementing the IHostUISupportsMultipleChoiceSelection interface. Resolves #318.
1 parent 98fd43b commit 6ce82f2

File tree

8 files changed

+174
-32
lines changed

8 files changed

+174
-32
lines changed

src/PowerShellEditorServices.Protocol/Messages/PromptEvents.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,22 @@ public static readonly
1313
RequestType<ShowChoicePromptRequest, ShowChoicePromptResponse> Type =
1414
RequestType<ShowChoicePromptRequest, ShowChoicePromptResponse>.Create("powerShell/showChoicePrompt");
1515

16+
public bool IsMultiChoice { get; set; }
17+
1618
public string Caption { get; set; }
1719

1820
public string Message { get; set; }
1921

2022
public ChoiceDetails[] Choices { get; set; }
2123

22-
public int DefaultChoice { get; set; }
24+
public int[] DefaultChoices { get; set; }
2325
}
2426

2527
public class ShowChoicePromptResponse
2628
{
2729
public bool PromptCancelled { get; set; }
2830

29-
public string ChosenItem { get; set; }
31+
public string ResponseText { get; set; }
3032
}
3133

3234
public class ShowInputPromptRequest

src/PowerShellEditorServices.Protocol/Server/PromptHandlers.cs

+7-3
Original file line numberDiff line numberDiff line change
@@ -40,30 +40,34 @@ public InputPromptHandler GetInputPromptHandler()
4040
}
4141
}
4242

43-
internal class ProtocolChoicePromptHandler : ChoicePromptHandler
43+
internal class ProtocolChoicePromptHandler : ConsoleChoicePromptHandler
4444
{
4545
private IMessageSender messageSender;
4646
private ConsoleService consoleService;
4747

4848
public ProtocolChoicePromptHandler(
4949
IMessageSender messageSender,
5050
ConsoleService consoleService)
51+
: base(consoleService)
5152
{
5253
this.messageSender = messageSender;
5354
this.consoleService = consoleService;
5455
}
5556

5657
protected override void ShowPrompt(PromptStyle promptStyle)
5758
{
59+
base.ShowPrompt(promptStyle);
60+
5861
messageSender
5962
.SendRequest(
6063
ShowChoicePromptRequest.Type,
6164
new ShowChoicePromptRequest
6265
{
66+
IsMultiChoice = this.IsMultiChoice,
6367
Caption = this.Caption,
6468
Message = this.Message,
6569
Choices = this.Choices,
66-
DefaultChoice = this.DefaultChoice
70+
DefaultChoices = this.DefaultChoices
6771
}, true)
6872
.ContinueWith(HandlePromptResponse)
6973
.ConfigureAwait(false);
@@ -79,7 +83,7 @@ private void HandlePromptResponse(
7983
if (!response.PromptCancelled)
8084
{
8185
this.consoleService.ReceivePromptResponse(
82-
response.ChosenItem,
86+
response.ResponseText,
8387
false);
8488
}
8589
else

src/PowerShellEditorServices/Console/ChoiceDetails.cs

+3
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ public static ChoiceDetails Create(ChoiceDescription choiceDescription)
118118
/// <returns>True if the input string is a match for the choice.</returns>
119119
public bool MatchesInput(string inputString)
120120
{
121+
// Make sure the input string is trimmed of whitespace
122+
inputString = inputString.Trim();
123+
121124
// Is it the hotkey?
122125
return
123126
string.Equals(inputString, this.hotKeyString, StringComparison.CurrentCultureIgnoreCase) ||

src/PowerShellEditorServices/Console/ChoicePromptHandler.cs

+87-14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
//
55

66
using System;
7+
using System.Collections.Generic;
8+
using System.Collections.ObjectModel;
9+
using System.Linq;
710
using System.Threading;
811
using System.Threading.Tasks;
912

@@ -37,12 +40,17 @@ public abstract class ChoicePromptHandler : PromptHandler
3740
{
3841
#region Private Fields
3942

40-
private TaskCompletionSource<int> promptTask;
43+
private TaskCompletionSource<int[]> promptTask;
4144

4245
#endregion
4346

4447
#region Properties
4548

49+
/// <summary>
50+
/// Returns true if the choice prompt allows multiple selections.
51+
/// </summary>
52+
protected bool IsMultiChoice { get; private set; }
53+
4654
/// <summary>
4755
/// Gets the caption (title) string to display with the prompt.
4856
/// </summary>
@@ -62,7 +70,7 @@ public abstract class ChoicePromptHandler : PromptHandler
6270
/// Gets the index of the default choice so that the user
6371
/// interface can make it easy to select this option.
6472
/// </summary>
65-
protected int DefaultChoice { get; private set; }
73+
protected int[] DefaultChoices { get; private set; }
6674

6775
#endregion
6876

@@ -102,18 +110,71 @@ public Task<int> PromptForChoice(
102110
this.Caption = promptCaption;
103111
this.Message = promptMessage;
104112
this.Choices = choices;
105-
this.DefaultChoice = defaultChoice;
106-
this.promptTask = new TaskCompletionSource<int>();
113+
this.promptTask = new TaskCompletionSource<int[]>();
114+
115+
this.DefaultChoices =
116+
defaultChoice == -1
117+
? new int[] { }
118+
: new int[] { defaultChoice };
107119

108120
// Cancel the TaskCompletionSource if the caller cancels the task
109121
cancellationToken.Register(this.CancelPrompt, true);
110122

111123
// Show the prompt to the user
112124
this.ShowPrompt(PromptStyle.Full);
113125

114-
return this.promptTask.Task;
126+
// Convert the int[] result to int
127+
return this.promptTask.Task.ContinueWith(
128+
t => t.Result.DefaultIfEmpty(-1).First());
115129
}
116130

131+
/// <summary>
132+
/// Prompts the user to make a choice of one or more options using the
133+
/// provided details.
134+
/// </summary>
135+
/// <param name="promptCaption">
136+
/// The caption string which will be displayed to the user.
137+
/// </param>
138+
/// <param name="promptMessage">
139+
/// The descriptive message which will be displayed to the user.
140+
/// </param>
141+
/// <param name="choices">
142+
/// The list of choices from which the user will select.
143+
/// </param>
144+
/// <param name="defaultChoices">
145+
/// The default choice(s) to highlight for the user.
146+
/// </param>
147+
/// <param name="cancellationToken">
148+
/// A CancellationToken that can be used to cancel the prompt.
149+
/// </param>
150+
/// <returns>
151+
/// A Task instance that can be monitored for completion to get
152+
/// the user's choices.
153+
/// </returns>
154+
public Task<int[]> PromptForChoice(
155+
string promptCaption,
156+
string promptMessage,
157+
ChoiceDetails[] choices,
158+
int[] defaultChoices,
159+
CancellationToken cancellationToken)
160+
{
161+
// TODO: Guard against multiple calls
162+
163+
this.Caption = promptCaption;
164+
this.Message = promptMessage;
165+
this.Choices = choices;
166+
this.DefaultChoices = defaultChoices;
167+
this.IsMultiChoice = true;
168+
this.promptTask = new TaskCompletionSource<int[]>();
169+
170+
// Cancel the TaskCompletionSource if the caller cancels the task
171+
cancellationToken.Register(this.CancelPrompt, true);
172+
173+
// Show the prompt to the user
174+
this.ShowPrompt(PromptStyle.Full);
175+
176+
return this.promptTask.Task;
177+
}
117178
/// <summary>
118179
/// Implements behavior to handle the user's response.
119180
/// </summary>
@@ -124,29 +185,41 @@ public Task<int> PromptForChoice(
124185
/// </returns>
125186
public override bool HandleResponse(string responseString)
126187
{
127-
int choiceIndex = -1;
188+
List<int> choiceIndexes = new List<int>();
128189

129-
// Clean up the response string
130-
responseString = responseString.Trim();
190+
// Clean up the response string and split it
191+
var choiceStrings =
192+
responseString.Trim().Split(
193+
new char[] { ',' },
194+
StringSplitOptions.RemoveEmptyEntries);
131195

132-
for (int i = 0; i < this.Choices.Length; i++)
196+
foreach (string choiceString in choiceStrings)
133197
{
134-
if (this.Choices[i].MatchesInput(responseString))
198+
for (int i = 0; i < this.Choices.Length; i++)
135199
{
136-
choiceIndex = i;
137-
break;
200+
if (this.Choices[i].MatchesInput(choiceString))
201+
{
202+
choiceIndexes.Add(i);
203+
204+
// If this is a single-choice prompt, break out after
205+
// the first matched choice
206+
if (!this.IsMultiChoice)
207+
{
208+
break;
209+
}
210+
}
138211
}
139212
}
140213

141-
if (choiceIndex == -1)
214+
if (choiceIndexes.Count == 0)
142215
{
143216
// The user did not respond with a valid choice,
144217
// show the prompt again to give another chance
145218
this.ShowPrompt(PromptStyle.Minimal);
146219
return false;
147220
}
148221

149-
this.promptTask.SetResult(choiceIndex);
222+
this.promptTask.SetResult(choiceIndexes.ToArray());
150223
return true;
151224
}
152225

src/PowerShellEditorServices/Console/ConsoleChoicePromptHandler.cs

+14-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
44
//
55

6+
using System.Linq;
7+
68
namespace Microsoft.PowerShell.EditorServices.Console
79
{
810
/// <summary>
@@ -71,12 +73,20 @@ protected override void ShowPrompt(PromptStyle promptStyle)
7173

7274
this.consoleHost.WriteOutput("[?] Help", false);
7375

74-
if (this.DefaultChoice > -1 && this.DefaultChoice < this.Choices.Length)
76+
var validDefaultChoices =
77+
this.DefaultChoices.Where(
78+
choice => choice > -1 && choice < this.Choices.Length);
79+
80+
if (validDefaultChoices.Any())
7581
{
82+
var choiceString =
83+
string.Join(
84+
", ",
85+
this.DefaultChoices
86+
.Select(choice => this.Choices[choice].Label));
87+
7688
this.consoleHost.WriteOutput(
77-
string.Format(
78-
" (default is \"{0}\"):",
79-
this.Choices[this.DefaultChoice].Label));
89+
$" (default is \"{choiceString}\"):");
8090
}
8191
}
8292

src/PowerShellEditorServices/Session/SessionPSHostUserInterface.cs

+45-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ namespace Microsoft.PowerShell.EditorServices
2121
/// for the ConsoleService and routes its calls to an IConsoleHost
2222
/// implementation.
2323
/// </summary>
24-
internal class ConsoleServicePSHostUserInterface : PSHostUserInterface
24+
internal class ConsoleServicePSHostUserInterface : PSHostUserInterface, IHostUISupportsMultipleChoiceSelection
2525
{
2626
#region Private Fields
2727

@@ -310,6 +310,50 @@ public override void WriteProgress(
310310

311311
#endregion
312312

313+
#region IHostUISupportsMultipleChoiceSelection Implementation
314+
315+
public Collection<int> PromptForChoice(
316+
string promptCaption,
317+
string promptMessage,
318+
Collection<ChoiceDescription> choiceDescriptions,
319+
IEnumerable<int> defaultChoices)
320+
{
321+
if (this.consoleHost != null)
322+
{
323+
ChoiceDetails[] choices =
324+
choiceDescriptions
325+
.Select(ChoiceDetails.Create)
326+
.ToArray();
327+
328+
CancellationTokenSource cancellationToken = new CancellationTokenSource();
329+
Task<int[]> promptTask =
330+
this.consoleHost
331+
.GetChoicePromptHandler()
332+
.PromptForChoice(
333+
promptCaption,
334+
promptMessage,
335+
choices,
336+
defaultChoices.ToArray(),
337+
cancellationToken.Token);
338+
339+
// Run the prompt task and wait for it to return
340+
this.WaitForPromptCompletion(
341+
promptTask,
342+
"PromptForChoice",
343+
cancellationToken);
344+
345+
// Return the result
346+
return new Collection<int>(promptTask.Result.ToList());
347+
}
348+
else
349+
{
350+
// Notify the caller that there's no implementation
351+
throw new NotImplementedException();
352+
}
353+
}
354+
355+
#endregion
356+
313357
#region Private Methods
314358

315359
private void WaitForPromptCompletion<TResult>(

test/PowerShellEditorServices.Test.Host/LanguageServerTests.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -526,17 +526,17 @@ public async Task ServiceExecutesReplCommandAndReceivesChoicePrompt()
526526
ShowChoicePromptRequest showChoicePromptRequest = requestResponseContext.Item1;
527527
RequestContext<ShowChoicePromptResponse> requestContext = requestResponseContext.Item2;
528528

529-
Assert.Equal(1, showChoicePromptRequest.DefaultChoice);
529+
Assert.Equal(1, showChoicePromptRequest.DefaultChoices[0]);
530530

531531
// Respond to the prompt request
532532
await requestContext.SendResult(
533533
new ShowChoicePromptResponse
534534
{
535-
ChosenItem = "a"
535+
ResponseText = "a"
536536
});
537537

538-
// Skip the initial script lines (6 script lines plus 2 blank lines)
539-
string[] outputLines = await outputReader.ReadLines(8);
538+
// Skip the initial script and prompt lines (6 script lines plus 2 blank lines and 3 prompt lines)
539+
string[] outputLines = await outputReader.ReadLines(11);
540540

541541
// Wait for the selection to appear as output
542542
await evaluateTask;

0 commit comments

Comments
 (0)