diff --git a/MockPSConsole/MockPSConsole.csproj b/MockPSConsole/MockPSConsole.csproj index 4bca152fc..47b8bf414 100644 --- a/MockPSConsole/MockPSConsole.csproj +++ b/MockPSConsole/MockPSConsole.csproj @@ -4,7 +4,7 @@ Exe MockPSConsole MockPSConsole - net461;net5.0 + net461;net6.0 512 Program.manifest true @@ -17,8 +17,8 @@ - - + + diff --git a/PSReadLine.build.ps1 b/PSReadLine.build.ps1 index 620019ffd..23ec7f425 100644 --- a/PSReadLine.build.ps1 +++ b/PSReadLine.build.ps1 @@ -19,7 +19,7 @@ param( [ValidateSet("Debug", "Release")] [string]$Configuration = (property Configuration Release), - [ValidateSet("net461", "net5.0")] + [ValidateSet("net461", "net6.0")] [string]$Framework, [switch]$CheckHelpContent @@ -32,7 +32,7 @@ $targetDir = "bin/$Configuration/PSReadLine" if (-not $Framework) { - $Framework = if ($PSVersionTable.PSEdition -eq "Core") { "net5.0" } else { "net461" } + $Framework = if ($PSVersionTable.PSEdition -eq "Core") { "net6.0" } else { "net461" } } Write-Verbose "Building for '$Framework'" -Verbose @@ -65,9 +65,9 @@ $mockPSConsoleParams = @{ Synopsis: Build the Polyfiller assembly #> task BuildPolyfiller @polyFillerParams -If ($Framework -eq "net461") { - ## Build both "net461" and "net5.0" + ## Build both "net461" and "net6.0" exec { dotnet publish -f "net461" -c $Configuration Polyfill } - exec { dotnet publish -f "net5.0" -c $Configuration Polyfill } + exec { dotnet publish -f "net6.0" -c $Configuration Polyfill } } <# @@ -132,12 +132,12 @@ task LayoutModule BuildPolyfiller, BuildMainModule, { if (-not (Test-Path "$targetDir/net461")) { New-Item "$targetDir/net461" -ItemType Directory -Force > $null } - if (-not (Test-Path "$targetDir/net5.0")) { - New-Item "$targetDir/net5.0" -ItemType Directory -Force > $null + if (-not (Test-Path "$targetDir/net6plus")) { + New-Item "$targetDir/net6plus" -ItemType Directory -Force > $null } Copy-Item "Polyfill/bin/$Configuration/net461/Microsoft.PowerShell.PSReadLine.Polyfiller.dll" "$targetDir/net461" -Force - Copy-Item "Polyfill/bin/$Configuration/net5.0/Microsoft.PowerShell.PSReadLine.Polyfiller.dll" "$targetDir/net5.0" -Force + Copy-Item "Polyfill/bin/$Configuration/net6.0/Microsoft.PowerShell.PSReadLine.Polyfiller.dll" "$targetDir/net6plus" -Force } $binPath = "PSReadLine/bin/$Configuration/$Framework/publish" diff --git a/PSReadLine/OnImportAndRemove.cs b/PSReadLine/OnImportAndRemove.cs index 653fa8e0c..644c9f09f 100644 --- a/PSReadLine/OnImportAndRemove.cs +++ b/PSReadLine/OnImportAndRemove.cs @@ -28,7 +28,7 @@ private static Assembly ResolveAssembly(object sender, ResolveEventArgs args) } string root = Path.GetDirectoryName(typeof(OnModuleImportAndRemove).Assembly.Location); - string subd = (Environment.Version.Major >= 5) ? "net5.0" : "net461"; + string subd = (Environment.Version.Major >= 6) ? "net6plus" : "net461"; string path = Path.Combine(root, subd, "Microsoft.PowerShell.PSReadLine.Polyfiller.dll"); return Assembly.LoadFrom(path); diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index ca7aa77e6..19f366513 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -143,7 +143,7 @@ private void SetOptionsInternal(SetPSReadLineOption options) } bool notTest = ReferenceEquals(_mockableMethods, this); - if ((options.PredictionSource & PredictionSource.Plugin) != 0 && Environment.Version.Major < 5 && notTest) + if ((options.PredictionSource & PredictionSource.Plugin) != 0 && Environment.Version.Major < 6 && notTest) { throw new ArgumentException(PSReadLineResources.PredictionPluginNotSupported); } diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index a01628906..e041fa0d4 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -9,7 +9,7 @@ 2.2.0 2.2.0-beta1 true - net461;net5.0 + net461;net6.0 true 9.0 @@ -20,8 +20,8 @@ - - + + diff --git a/PSReadLine/PSReadLine.sln b/PSReadLine/PSReadLine.sln index 93c5b6c7e..3ce7ce89f 100644 --- a/PSReadLine/PSReadLine.sln +++ b/PSReadLine/PSReadLine.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 15.0.26730.12 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSReadLine", "PSReadLine.csproj", "{615788CB-1B9A-4B34-97B3-4608686E59CA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Polyfill", "..\Polyfill\Polyfill.csproj", "{DE521A7D-A3BE-4A07-BE75-5AB7D87E799D}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MockPSConsole", "..\MockPSConsole\MockPSConsole.csproj", "{08218B1A-8B85-4722-9E3F-4D6C0BF58AD8}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSReadLine.Tests", "..\test\PSReadLine.Tests.csproj", "{8ED51D01-158C-4B29-824A-35B9B861E45A}" diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index bde0f9e37..b576eb53f 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -705,7 +705,7 @@ internal static string PredictiveSuggestionNotSupported { } /// - /// Looks up a localized string similar to The prediction plugin source is not supported in this version of PowerShell. The 7.1 or a higher version of PowerShell is required to use this source. + /// Looks up a localized string similar to The prediction plugin source is not supported in this version of PowerShell. The 7.2 or a higher version of PowerShell is required to use this source. /// internal static string PredictionPluginNotSupported { diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index 1d042bda4..7c9044318 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -815,7 +815,7 @@ Or not saving history with: The predictive suggestion feature cannot be enabled because the console output doesn't support virtual terminal processing or it's redirected. - The prediction plugin source is not supported in this version of PowerShell. The 7.1 or a higher version of PowerShell is required to use this source. + The prediction plugin source is not supported in this version of PowerShell. The 7.2 or a higher version of PowerShell is required to use this source. Delete the current logical line and up to the end of the multiline buffer diff --git a/PSReadLine/Prediction.Entry.cs b/PSReadLine/Prediction.Entry.cs index 4d619727c..a60d05e2e 100644 --- a/PSReadLine/Prediction.Entry.cs +++ b/PSReadLine/Prediction.Entry.cs @@ -15,14 +15,21 @@ public partial class PSConsoleReadLine private struct SuggestionEntry { internal readonly Guid PredictorId; + internal readonly uint? PredictorSession; internal readonly string Source; internal readonly string SuggestionText; internal readonly int InputMatchIndex; - internal SuggestionEntry(string soruce, Guid id, string suggestion, int matchIndex) + internal SuggestionEntry(string suggestion, int matchIndex) + : this(source: "History", predictorId: Guid.Empty, predictorSession: null, suggestion, matchIndex) { - Source = soruce; - PredictorId = id; + } + + internal SuggestionEntry(string source, Guid predictorId, uint? predictorSession, string suggestion, int matchIndex) + { + Source = source; + PredictorId = predictorId; + PredictorSession = predictorSession; SuggestionText = suggestion; InputMatchIndex = matchIndex; } diff --git a/PSReadLine/Prediction.Views.cs b/PSReadLine/Prediction.Views.cs index f585194a2..ad207e95b 100644 --- a/PSReadLine/Prediction.Views.cs +++ b/PSReadLine/Prediction.Views.cs @@ -128,7 +128,6 @@ protected string GetOneHistorySuggestion(string text) /// Maximum number of results to return. protected List GetHistorySuggestions(string input, int count) { - const string source = "History"; List results = null; int remainingCount = count; @@ -164,7 +163,7 @@ protected List GetHistorySuggestions(string input, int count) _cacheHistorySet.Add(line); if (matchIndex == 0) { - results.Add(new SuggestionEntry(source, Guid.Empty, line, matchIndex)); + results.Add(new SuggestionEntry(line, matchIndex)); if (--remainingCount == 0) { break; @@ -172,7 +171,7 @@ protected List GetHistorySuggestions(string input, int count) } else if (_cacheHistoryList.Count < remainingCount) { - _cacheHistoryList.Add(new SuggestionEntry(source, Guid.Empty, line, matchIndex)); + _cacheHistoryList.Add(new SuggestionEntry(line, matchIndex)); } } @@ -440,11 +439,19 @@ private void AggregateSuggestions() break; } - for (int i = 0; i < _cacheList2[index]; i++) + int num = _cacheList2[index]; + for (int i = 0; i < num; i++) { string sugText = item.Suggestions[i].SuggestionText ?? string.Empty; int matchIndex = sugText.IndexOf(_inputText, comparison); - _listItems.Add(new SuggestionEntry(item.Name, item.Id, sugText, matchIndex)); + _listItems.Add(new SuggestionEntry(item.Name, item.Id, item.Session, sugText, matchIndex)); + } + + if (item.Session.HasValue) + { + // Send feedback only if the mini-session id is specified. + // When it's not specified, we consider the predictor doesn't accept feedback. + _singleton._mockableMethods.OnSuggestionDisplayed(item.Id, item.Session.Value, num); } } } @@ -517,9 +524,11 @@ internal override void OnSuggestionAccepted() if (_listItems != null && _selectedIndex != -1) { var item = _listItems[_selectedIndex]; - if (item.PredictorId != Guid.Empty) + if (item.PredictorSession.HasValue) { - _singleton._mockableMethods.OnSuggestionAccepted(item.PredictorId, item.SuggestionText); + // Send feedback only if the mini-session id is specified. + // When it's not specified, we consider the predictor doesn't accept feedback. + _singleton._mockableMethods.OnSuggestionAccepted(item.PredictorId, item.PredictorSession.Value, item.SuggestionText); } } } @@ -577,6 +586,7 @@ internal void UpdateListSelection(int move) private class PredictionInlineView : PredictionViewBase { private Guid _predictorId; + private uint? _predictorSession; private string _suggestionText; private string _lastInputText; private bool _alreadyAccepted; @@ -613,6 +623,7 @@ internal override void GetSuggestion(string userInput) { _suggestionText = GetOneHistorySuggestion(userInput); _predictorId = Guid.Empty; + _predictorSession = null; } } } @@ -634,15 +645,27 @@ private void AggregateSuggestions() continue; } + int index = 0; foreach (var sug in item.Suggestions) { if (sug.SuggestionText != null && sug.SuggestionText.StartsWith(_inputText, _singleton._options.HistoryStringComparison)) { _predictorId = item.Id; + _predictorSession = item.Session; _suggestionText = sug.SuggestionText; + + if (_predictorSession.HasValue) + { + // Send feedback only if the mini-session id is specified. + // When it's not specified, we consider the predictor doesn't accept feedback. + _singleton._mockableMethods.OnSuggestionDisplayed(_predictorId, _predictorSession.Value, -index); + } + return; } + + index++; } } } @@ -673,10 +696,13 @@ internal override void OnSuggestionAccepted() return; } - if (!_alreadyAccepted && _suggestionText != null && _predictorId != Guid.Empty) + if (!_alreadyAccepted && _suggestionText != null && _predictorSession.HasValue) { _alreadyAccepted = true; - _singleton._mockableMethods.OnSuggestionAccepted(_predictorId, _suggestionText); + + // Send feedback only if the mini-session id is specified. + // When it's not specified, we consider the predictor doesn't accept feedback. + _singleton._mockableMethods.OnSuggestionAccepted(_predictorId, _predictorSession.Value, _suggestionText); } } @@ -725,6 +751,7 @@ internal override void Reset() base.Reset(); _suggestionText = _lastInputText = null; _predictorId = Guid.Empty; + _predictorSession = null; _alreadyAccepted = false; } diff --git a/PSReadLine/Prediction.cs b/PSReadLine/Prediction.cs index aa33fb8c6..597cb02c5 100644 --- a/PSReadLine/Prediction.cs +++ b/PSReadLine/Prediction.cs @@ -16,23 +16,31 @@ namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { + private const string PSReadLine = "PSReadLine"; + // Stub helper methods so prediction can be mocked [ExcludeFromCodeCoverage] Task> IPSConsoleReadLineMockableMethods.PredictInput(Ast ast, Token[] tokens) { - return CommandPrediction.PredictInput(ast, tokens); + return CommandPrediction.PredictInput(PSReadLine, ast, tokens); } [ExcludeFromCodeCoverage] - void IPSConsoleReadLineMockableMethods.OnCommandLineAccepted(IReadOnlyList history) + void IPSConsoleReadLineMockableMethods.OnSuggestionDisplayed(Guid predictorId, uint session, int countOrIndex) { - CommandPrediction.OnCommandLineAccepted(history); + CommandPrediction.OnSuggestionDisplayed(PSReadLine, predictorId, session, countOrIndex); } [ExcludeFromCodeCoverage] - void IPSConsoleReadLineMockableMethods.OnSuggestionAccepted(Guid predictorId, string suggestionText) + void IPSConsoleReadLineMockableMethods.OnSuggestionAccepted(Guid predictorId, uint session, string suggestionText) + { + CommandPrediction.OnSuggestionAccepted(PSReadLine, predictorId, session, suggestionText); + } + + [ExcludeFromCodeCoverage] + void IPSConsoleReadLineMockableMethods.OnCommandLineAccepted(IReadOnlyList history) { - CommandPrediction.OnSuggestionAccepted(predictorId, suggestionText); + CommandPrediction.OnCommandLineAccepted(PSReadLine, history); } private readonly Prediction _prediction; diff --git a/PSReadLine/PublicAPI.cs b/PSReadLine/PublicAPI.cs index 945c45a36..e661b29a9 100644 --- a/PSReadLine/PublicAPI.cs +++ b/PSReadLine/PublicAPI.cs @@ -28,7 +28,8 @@ public interface IPSConsoleReadLineMockableMethods bool RunspaceIsRemote(Runspace runspace); Task> PredictInput(Ast ast, Token[] tokens); void OnCommandLineAccepted(IReadOnlyList history); - void OnSuggestionAccepted(Guid predictorId, string suggestionText); + void OnSuggestionDisplayed(Guid predictorId, uint session, int countOrIndex); + void OnSuggestionAccepted(Guid predictorId, uint session, string suggestionText); void RenderFullHelp(string content, string regexPatternToScrollTo); object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp); } diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs index 62e7c2227..c3912371a 100644 --- a/PSReadLine/ReadLine.cs +++ b/PSReadLine/ReadLine.cs @@ -668,7 +668,7 @@ private PSConsoleReadLine() } if (hostName == null) { - hostName = "PSReadLine"; + hostName = PSReadLine; } _options = new PSConsoleReadLineOptions(hostName); _prediction = new Prediction(this); diff --git a/Polyfill/CommandPrediction.cs b/Polyfill/CommandPrediction.cs index b5792086e..3b0c18aa6 100644 --- a/Polyfill/CommandPrediction.cs +++ b/Polyfill/CommandPrediction.cs @@ -23,16 +23,24 @@ public sealed class PredictionResult [HiddenAttribute] public string Name { get; } + /// + /// Gets the mini-session id that represents a specific invocation that returns this result. + /// When it's not specified, it's considered by a client that the predictor doesn't expect feedback. + /// + [HiddenAttribute] + public uint? Session { get; } + /// /// Gets the suggestions. /// [HiddenAttribute] public IReadOnlyList Suggestions { get; } - internal PredictionResult(Guid id, string name, List suggestions) + internal PredictionResult(Guid id, string name, uint? session, List suggestions) { Id = id; Name = name; + Session = session; Suggestions = suggestions; } } @@ -90,11 +98,12 @@ public static class CommandPrediction /// /// Collect the predictive suggestions from registered predictors using the default timeout. /// + /// Represents the client that initiates the call. /// The object from parsing the current command line input. /// The objects from parsing the current command line input. /// A list of objects. [HiddenAttribute] - public static Task> PredictInput(Ast ast, Token[] astTokens) + public static Task> PredictInput(string client, Ast ast, Token[] astTokens) { return null; } @@ -102,12 +111,13 @@ public static Task> PredictInput(Ast ast, Token[] astToke /// /// Collect the predictive suggestions from registered predictors using the specified timeout. /// + /// Represents the client that initiates the call. /// The object from parsing the current command line input. /// The objects from parsing the current command line input. /// The milliseconds to timeout. /// A list of objects. [HiddenAttribute] - public static Task> PredictInput(Ast ast, Token[] astTokens, int millisecondsTimeout) + public static Task> PredictInput(string client, Ast ast, Token[] astTokens, int millisecondsTimeout) { return null; } @@ -115,19 +125,37 @@ public static Task> PredictInput(Ast ast, Token[] astToke /// /// Allow registered predictors to do early processing when a command line is accepted. /// + /// Represents the client that initiates the call. /// History command lines provided as references for prediction. [HiddenAttribute] - public static void OnCommandLineAccepted(IReadOnlyList history) + public static void OnCommandLineAccepted(string client, IReadOnlyList history) + { + } + + /// + /// Send feedback to a predictor when one or more suggestions from it were displayed to the user. + /// + /// Represents the client that initiates the call. + /// The identifier of the predictor whose prediction result was accepted. + /// The mini-session where the displayed suggestions came from. + /// + /// When the value is > 0, it's the number of displayed suggestions from the list returned in , starting from the index 0. + /// When the value is <= 0, it means a single suggestion from the list got displayed, and the index is the absolute value. + /// + [HiddenAttribute] + public static void OnSuggestionDisplayed(string client, Guid predictorId, uint session, int countOrIndex) { } /// /// Send feedback to predictors about their last suggestions. /// + /// Represents the client that initiates the call. /// The identifier of the predictor whose prediction result was accepted. + /// The mini-session where the accepted suggestion came from. /// The accepted suggestion text. [HiddenAttribute] - public static void OnSuggestionAccepted(Guid predictorId, string suggestionText) + public static void OnSuggestionAccepted(string client, Guid predictorId, uint session, string suggestionText) { } } diff --git a/Polyfill/Polyfill.csproj b/Polyfill/Polyfill.csproj index 95cafac7d..1e0ff4d44 100644 --- a/Polyfill/Polyfill.csproj +++ b/Polyfill/Polyfill.csproj @@ -3,7 +3,7 @@ Microsoft.PowerShell.PSReadLine.Polyfiller 1.0.0.0 - net461;net5.0 + net461;net6.0 true @@ -11,8 +11,8 @@ - - + + diff --git a/build.ps1 b/build.ps1 index c3461c877..25e935bc0 100644 --- a/build.ps1 +++ b/build.ps1 @@ -39,7 +39,7 @@ param( [ValidateSet("Debug", "Release")] [string] $Configuration = "Debug", - [ValidateSet("net461", "net5.0")] + [ValidateSet("net461", "net6.0")] [string] $Framework ) diff --git a/test/InlinePredictionTest.cs b/test/InlinePredictionTest.cs index 91cc03a61..43fd485e9 100644 --- a/test/InlinePredictionTest.cs +++ b/test/InlinePredictionTest.cs @@ -364,6 +364,7 @@ public void Inline_AcceptSuggestionInVIMode() )); } + private const uint MiniSessionId = 56; private static readonly Guid predictorId_1 = Guid.Parse("b45b5fbe-90fa-486c-9c87-e7940fdd6273"); private static readonly Guid predictorId_2 = Guid.Parse("74a86463-033b-44a3-b386-41ee191c94be"); @@ -374,7 +375,7 @@ internal static List MockedPredictInput(Ast ast, Token[] token { var ctor = typeof(PredictionResult).GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, null, - new[] { typeof(Guid), typeof(string), typeof(List) }, null); + new[] { typeof(Guid), typeof(string), typeof(uint), typeof(List) }, null); var input = ast.Extent.Text; if (input == "netsh") @@ -395,9 +396,9 @@ internal static List MockedPredictInput(Ast ast, Token[] token return new List { (PredictionResult)ctor.Invoke( - new object[] { predictorId_1, "TestPredictor", suggestions_1 }), + new object[] { predictorId_1, "TestPredictor", MiniSessionId, suggestions_1 }), (PredictionResult)ctor.Invoke( - new object[] { predictorId_2, "LongNamePredictor", suggestions_2 }), + new object[] { predictorId_2, "LongNamePredictor", MiniSessionId, suggestions_2 }), }; } @@ -416,11 +417,15 @@ public void Inline_PluginSource_Acceptance() "git", CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "git", TokenClassification.InlinePrediction, " SOME TEXT AFTER")), + // `OnSuggestionDisplayed` should be fired for only one predictor because we are in 'inline' view. + CheckThat(() => AssertDisplayedSuggestions(count: 1, predictorId_1, MiniSessionId, -1)), + CheckThat(() => _mockedMethods.ClearPredictionFields()), // 'ctrl+f' will trigger 'OnSuggestionAccepted'. _.Ctrl_f, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "git", TokenClassification.None, " SOME ", TokenClassification.InlinePrediction, "TEXT AFTER")), + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), CheckThat(() => Assert.Equal(predictorId_1, _mockedMethods.acceptedPredictorId)), CheckThat(() => Assert.Equal("git SOME TEXT AFTER", _mockedMethods.acceptedSuggestion)), CheckThat(() => Assert.Null(_mockedMethods.commandHistory)), @@ -430,18 +435,21 @@ public void Inline_PluginSource_Acceptance() TokenClassification.Command, "git", TokenClassification.None, " SOME TEXT ", TokenClassification.InlinePrediction, "AFTER")), + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), CheckThat(() => Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId)), CheckThat(() => Assert.Null(_mockedMethods.acceptedSuggestion)), CheckThat(() => Assert.Null(_mockedMethods.commandHistory)), _.RightArrow, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "git", TokenClassification.None, " SOME TEXT AFTER")), + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), CheckThat(() => Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId)), CheckThat(() => Assert.Null(_mockedMethods.acceptedSuggestion)), CheckThat(() => Assert.Null(_mockedMethods.commandHistory)) )); // 'Enter' will trigger 'OnCommandLineAccepted'. + Assert.Empty(_mockedMethods.displayedSuggestions); Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId); Assert.Null(_mockedMethods.acceptedSuggestion); Assert.NotNull(_mockedMethods.commandHistory); @@ -458,6 +466,8 @@ public void Inline_PluginSource_Acceptance() )); // 'Enter' will trigger 'OnCommandLineAccepted', because plugin is in use. + // Also, we still have `OnSuggestionDisplayed` fired, from the typing of each character of `nets`. + AssertDisplayedSuggestions(count: 1, predictorId_1, MiniSessionId, -1); Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId); Assert.Null(_mockedMethods.acceptedSuggestion); Assert.NotNull(_mockedMethods.commandHistory); @@ -482,10 +492,14 @@ public void Inline_HistoryAndPluginSource_Acceptance() "git", CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "git", TokenClassification.InlinePrediction, " SOME TEXT AFTER")), + // `OnSuggestionDisplayed` should be fired for only one predictor because we are in 'inline' view. + CheckThat(() => AssertDisplayedSuggestions(count: 1, predictorId_1, MiniSessionId, -1)), + CheckThat(() => _mockedMethods.ClearPredictionFields()), _.Ctrl_f, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "git", TokenClassification.None, " SOME ", TokenClassification.InlinePrediction, "TEXT AFTER")), + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), CheckThat(() => Assert.Equal(predictorId_1, _mockedMethods.acceptedPredictorId)), CheckThat(() => Assert.Equal("git SOME TEXT AFTER", _mockedMethods.acceptedSuggestion)), CheckThat(() => Assert.Null(_mockedMethods.commandHistory)), @@ -494,17 +508,20 @@ public void Inline_HistoryAndPluginSource_Acceptance() TokenClassification.Command, "git", TokenClassification.None, " SOME TEXT ", TokenClassification.InlinePrediction, "AFTER")), + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), CheckThat(() => Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId)), CheckThat(() => Assert.Null(_mockedMethods.acceptedSuggestion)), CheckThat(() => Assert.Null(_mockedMethods.commandHistory)), _.RightArrow, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "git", TokenClassification.None, " SOME TEXT AFTER")), + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), CheckThat(() => Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId)), CheckThat(() => Assert.Null(_mockedMethods.acceptedSuggestion)), CheckThat(() => Assert.Null(_mockedMethods.commandHistory)) )); + Assert.Empty(_mockedMethods.displayedSuggestions); Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId); Assert.Null(_mockedMethods.acceptedSuggestion); Assert.NotNull(_mockedMethods.commandHistory); @@ -520,11 +537,15 @@ public void Inline_HistoryAndPluginSource_Acceptance() "netsh", CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "netsh", TokenClassification.InlinePrediction, " show me")), + // Yeah, we still have `OnSuggestionDisplayed` fired, from the typing of each character of `nets`. + CheckThat(() => AssertDisplayedSuggestions(count: 1, predictorId_1, MiniSessionId, -1)), + CheckThat(() => _mockedMethods.ClearPredictionFields()), // 'ctrl+f' won't trigger 'OnSuggestionAccepted' as the suggestion is from history. _.Ctrl_f, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "netsh", TokenClassification.None, " show ", TokenClassification.InlinePrediction, "me")), + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), CheckThat(() => Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId)), CheckThat(() => Assert.Null(_mockedMethods.acceptedSuggestion)), CheckThat(() => Assert.Null(_mockedMethods.commandHistory)), @@ -532,11 +553,13 @@ public void Inline_HistoryAndPluginSource_Acceptance() _.RightArrow, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "netsh", TokenClassification.None, " show me")), + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), CheckThat(() => Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId)), CheckThat(() => Assert.Null(_mockedMethods.acceptedSuggestion)), CheckThat(() => Assert.Null(_mockedMethods.commandHistory)) )); + Assert.Empty(_mockedMethods.displayedSuggestions); Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId); Assert.Null(_mockedMethods.acceptedSuggestion); Assert.NotNull(_mockedMethods.commandHistory); diff --git a/test/ListPredictionTest.cs b/test/ListPredictionTest.cs index 24545122d..82c6978dd 100644 --- a/test/ListPredictionTest.cs +++ b/test/ListPredictionTest.cs @@ -37,6 +37,15 @@ private Disposable SetPrediction(PredictionSource source, PredictionViewStyle vi new SetPSReadLineOption { PredictionSource = oldSource, PredictionViewStyle = oldView })); } + private void AssertDisplayedSuggestions(int count, Guid predictorId, uint session, int countOrIndex) + { + Assert.Equal(count, _mockedMethods.displayedSuggestions.Count); + _mockedMethods.displayedSuggestions.TryGetValue(predictorId, out var tuple); + Assert.NotNull(tuple); + Assert.Equal(session, tuple.Item1); + Assert.Equal(countOrIndex, tuple.Item2); + } + [SkippableFact] public void List_RenderSuggestion_NoMatching() { @@ -958,6 +967,10 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should be fired for both predictors. + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_1, MiniSessionId, 2)), + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_2, MiniSessionId, 1)), + CheckThat(() => _mockedMethods.ClearPredictionFields()), _.DownArrow, CheckThat(() => AssertScreenIs(5, TokenClassification.Command, "SOME", @@ -990,6 +1003,8 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should not be fired when navigating the list. + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), _.Shift_Home, CheckThat(() => AssertScreenIs(5, TokenClassification.Selection, "SOME TEXT BEFORE ec", @@ -1021,6 +1036,8 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should not be fired when selecting the input. + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), "j", CheckThat(() => AssertScreenIs(5, TokenClassification.Command, "j", @@ -1052,6 +1069,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should be fired for both predictors. + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_1, MiniSessionId, 2)), + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_2, MiniSessionId, 1)), CheckThat(() => Assert.Equal(predictorId_1, _mockedMethods.acceptedPredictorId)), CheckThat(() => Assert.Equal("SOME TEXT BEFORE ec", _mockedMethods.acceptedSuggestion)), CheckThat(() => Assert.Null(_mockedMethods.commandHistory)), @@ -1090,6 +1110,8 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should not be fired when navigating the input. + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), _.Backspace, CheckThat(() => AssertScreenIs(5, TokenClassification.Command, "SOME", @@ -1124,6 +1146,9 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should be fired for both predictors. + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_1, MiniSessionId, 2)), + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_2, MiniSessionId, 1)), CheckThat(() => Assert.Equal(predictorId_2, _mockedMethods.acceptedPredictorId)), CheckThat(() => Assert.Equal("SOME NEW TEXT", _mockedMethods.acceptedSuggestion)), CheckThat(() => Assert.Null(_mockedMethods.commandHistory)), @@ -1163,6 +1188,8 @@ public void List_PluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should not be fired when navigating the input. + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), // Once accepted, the list should be cleared. _.Enter, CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "SOME", @@ -1171,6 +1198,8 @@ public void List_PluginSource_Acceptance() TokenClassification.None, new string(' ', windowWidth))) )); + // `OnSuggestionDisplayed` should not be fired when 'Enter' accepting the input. + Assert.Empty(_mockedMethods.displayedSuggestions); Assert.Equal(predictorId_1, _mockedMethods.acceptedPredictorId); Assert.Equal("SOME NEW TEX SOME TEXT AFTER", _mockedMethods.acceptedSuggestion); Assert.Equal(3, _mockedMethods.commandHistory.Count); @@ -1240,6 +1269,10 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should be fired for both predictors. + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_1, MiniSessionId, 2)), + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_2, MiniSessionId, 1)), + CheckThat(() => _mockedMethods.ClearPredictionFields()), _.DownArrow, _.Shift_Home, CheckThat(() => AssertScreenIs(7, TokenClassification.Selection, "eca -zoo", @@ -1289,6 +1322,8 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should not be fired when navigating the list. + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), 'j', CheckThat(() => AssertScreenIs(6, TokenClassification.Command, "j", NextLine, @@ -1328,6 +1363,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should be fired for both predictors. + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_1, MiniSessionId, 2)), + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_2, MiniSessionId, 1)), // Update the selected item won't trigger 'acceptance' callbacks if the item is from history. CheckThat(() => Assert.Equal(Guid.Empty, _mockedMethods.acceptedPredictorId)), CheckThat(() => Assert.Null(_mockedMethods.acceptedSuggestion)), @@ -1374,6 +1412,8 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should not be fired when navigating the list. + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), _.Backspace, CheckThat(() => AssertScreenIs(5, TokenClassification.Command, "SOME", @@ -1408,6 +1448,9 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should be fired for both predictors. + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_1, MiniSessionId, 2)), + CheckThat(() => AssertDisplayedSuggestions(count: 2, predictorId_2, MiniSessionId, 1)), CheckThat(() => Assert.Equal(predictorId_2, _mockedMethods.acceptedPredictorId)), CheckThat(() => Assert.Equal("SOME NEW TEXT", _mockedMethods.acceptedSuggestion)), CheckThat(() => Assert.Null(_mockedMethods.commandHistory)), @@ -1447,6 +1490,8 @@ public void List_HistoryAndPluginSource_Acceptance() NextLine, TokenClassification.None, new string(' ', windowWidth) )), + // `OnSuggestionDisplayed` should not be fired when navigating the list. + CheckThat(() => Assert.Empty(_mockedMethods.displayedSuggestions)), // Once accepted, the list should be cleared. _.Enter, CheckThat(() => AssertScreenIs(2, TokenClassification.Command, "SOME", @@ -1455,6 +1500,7 @@ public void List_HistoryAndPluginSource_Acceptance() TokenClassification.None, new string(' ', windowWidth))) )); + Assert.Empty(_mockedMethods.displayedSuggestions); Assert.Equal(predictorId_1, _mockedMethods.acceptedPredictorId); Assert.Equal("SOME NEW TEX SOME TEXT AFTER", _mockedMethods.acceptedSuggestion); Assert.Equal(4, _mockedMethods.commandHistory.Count); diff --git a/test/PSReadLine.Tests.csproj b/test/PSReadLine.Tests.csproj index 4c1a993d1..c27ce274a 100644 --- a/test/PSReadLine.Tests.csproj +++ b/test/PSReadLine.Tests.csproj @@ -5,7 +5,7 @@ library UnitTestPSReadLine PSReadLine.Tests - net461;net5.0 + net461;net6.0 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} False @@ -23,8 +23,8 @@ - - + + diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index 21d05ea4e..e3c5dd06b 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -26,12 +26,14 @@ internal class MockedMethods : IPSConsoleReadLineMockableMethods internal Guid acceptedPredictorId; internal string acceptedSuggestion; internal string helpContentRendered; + internal Dictionary> displayedSuggestions = new Dictionary>(); internal void ClearPredictionFields() { commandHistory = null; acceptedPredictorId = Guid.Empty; acceptedSuggestion = null; + displayedSuggestions.Clear(); } public void Ding() @@ -63,7 +65,12 @@ public void OnCommandLineAccepted(IReadOnlyList history) commandHistory = history; } - public void OnSuggestionAccepted(Guid predictorId, string suggestionText) + public void OnSuggestionDisplayed(Guid predictorId, uint session, int countOrIndex) + { + displayedSuggestions[predictorId] = Tuple.Create(session, countOrIndex); + } + + public void OnSuggestionAccepted(Guid predictorId, uint session, string suggestionText) { acceptedPredictorId = predictorId; acceptedSuggestion = suggestionText; diff --git a/tools/helper.psm1 b/tools/helper.psm1 index d941a5fa0..c5e4e0d9e 100644 --- a/tools/helper.psm1 +++ b/tools/helper.psm1 @@ -1,5 +1,5 @@ -$MinimalSDKVersion = '5.0.100' +$MinimalSDKVersion = '6.0.100-preview.1.21103.13' $IsWindowsEnv = [System.Environment]::OSVersion.Platform -eq "Win32NT" $RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path $LocalDotnetDirPath = if ($IsWindowsEnv) { "$env:LocalAppData\Microsoft\dotnet" } else { "$env:HOME/.dotnet" }