From cb1452646b02c1e2847eaff95f415c251e1dbf82 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 30 Apr 2020 14:31:11 -0700 Subject: [PATCH 1/6] Add basic support for inline predictive suggestion --- PSReadLine/BasicEditing.cs | 34 +++++---- PSReadLine/Changes.txt | 23 ++++++ PSReadLine/Cmdlets.cs | 37 +++++++++- PSReadLine/Completion.cs | 3 + PSReadLine/History.cs | 22 +++++- PSReadLine/Movement.cs | 15 +++- PSReadLine/Options.cs | 4 + PSReadLine/PSReadLine.csproj | 7 +- PSReadLine/PSReadLine.format.ps1xml | 7 ++ PSReadLine/PublicAPI.cs | 29 ++++++++ PSReadLine/ReadLine.vi.cs | 6 ++ PSReadLine/Render.cs | 60 +++++++++++---- PSReadLine/Suggestion.cs | 61 +++++++++++++++ PSReadLine/Words.cs | 51 +++++++++++++ docs/Set-PSReadLineOption.md | 25 +++++++ test/SuggestionTest.cs | 110 ++++++++++++++++++++++++++++ test/UnitTestReadLine.cs | 8 +- 17 files changed, 465 insertions(+), 37 deletions(-) create mode 100644 PSReadLine/Suggestion.cs create mode 100644 test/SuggestionTest.cs diff --git a/PSReadLine/BasicEditing.cs b/PSReadLine/BasicEditing.cs index bbfbd39ec..492e59b68 100644 --- a/PSReadLine/BasicEditing.cs +++ b/PSReadLine/BasicEditing.cs @@ -31,26 +31,20 @@ public static void SelfInsert(ConsoleKeyInfo? key = null, object arg = null) { if (count <= 0) return; - if (count > 1) - { - var toInsert = new string(keyChar, count); - if (_singleton._visualSelectionCommandCount > 0) - { - _singleton.GetRegion(out var start, out var length); - Replace(start, length, toInsert); - } - else - { - Insert(toInsert); - } - return; - } + } + else + { + count = 1; } if (_singleton._visualSelectionCommandCount > 0) { _singleton.GetRegion(out var start, out var length); - Replace(start, length, new string(keyChar, 1)); + Replace(start, length, new string(keyChar, count)); + } + else if (count > 1) + { + Insert(new string(keyChar, count)); } else { @@ -85,6 +79,8 @@ public static void CancelLine(ConsoleKeyInfo? key = null, object arg = null) { _singleton.ClearStatusMessage(false); _singleton._current = _singleton._buffer.Length; + + using var _ = _singleton.PredictionOff(); _singleton.ForceRender(); _singleton._console.Write("\x1b[91m^C\x1b[0m"); @@ -214,6 +210,8 @@ public static void DeleteCharOrExit(ConsoleKeyInfo? key = null, object arg = nul private bool AcceptLineImpl(bool validate) { + using var _ = PredictionOff(); + ParseInput(); if (_parseErrors.Any(e => e.IncompleteInput)) { @@ -274,6 +272,12 @@ private bool AcceptLineImpl(bool validate) // Let public API set cursor to end of line incase end of line is end of buffer SetCursorPosition(_current); + if (_suggestionText != null) + { + _suggestionText = null; + _console.BlankRestOfLine(); + } + _console.Write("\n"); _inputAccepted = true; return true; diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index dbef3b6b6..637f5c875 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -1,3 +1,26 @@ +### Version 2.1.0-beta1 + +Pre-release notes: + +#### Prototype to support the fish-like auto-suggestion in PSReadLine + +Currently: + +- The only suggestion source is the PSReadLine history file. + The ultimate goal is to call into a prediction API in PowerShell engine that is powered by a prediction plugin. +- By default, the suggestion text is rendered with the `dark black` color `\x1b[38;5;238m`, which works OK with a black background. + You can change it by running `Set-PSReadLineOption -Colors @{ Prediction = '' }`. +- VI mode - now the suggestion is disabled when switching to the command mode, and enabled when switching to insert mode. +- Very limited efforts have been spent on the VI/Emacs experience, so it could be pretty raw for them, and feedback is very welcome. + +When using this PSReadLine, please add the following key binding to your profile: + + Set-PSReadLineKeyHandler -Key "Ctrl+f" -Function ForwardWord + +- Pressing `RightArrow` at the end of current typing will accept the suggestion text +- Pressing `Ctrl+f` at the end of current typing will accept a word from the suggestion text +- Pressing `Ctrl+z` will get you back to where you were before accepting all or part of the suggestion + ### Version 2.0.1 Bug fixes: diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index c3d569b65..7fe7d7382 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -63,6 +63,12 @@ public enum AddToHistoryOption MemoryAndFile } + public enum PredictionStyle + { + None, + Concise, + } + public class PSConsoleReadLineOptions { public const ConsoleColor DefaultCommentColor = ConsoleColor.DarkGreen; @@ -78,6 +84,10 @@ public class PSConsoleReadLineOptions public const ConsoleColor DefaultEmphasisColor = ConsoleColor.Cyan; public const ConsoleColor DefaultErrorColor = ConsoleColor.Red; + // Use dark black by default for the suggestion text. + // Find the most suitable color using https://stackoverflow.com/a/33206814 + public const string DefaultPredictionColor = "\x1b[38;5;238m"; + public static EditMode DefaultEditMode = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? EditMode.Windows : EditMode.Emacs; @@ -128,6 +138,8 @@ public class PSConsoleReadLineOptions public const HistorySaveStyle DefaultHistorySaveStyle = HistorySaveStyle.SaveIncrementally; + public const PredictionStyle DefaultPredictionStyle = PredictionStyle.Concise; + /// /// How long in milliseconds should we wait before concluding /// the input is not an escape sequence? @@ -155,6 +167,7 @@ public PSConsoleReadLineOptions(string hostName) HistorySearchCaseSensitive = DefaultHistorySearchCaseSensitive; HistorySaveStyle = DefaultHistorySaveStyle; AnsiEscapeTimeout = DefaultAnsiEscapeTimeout; + PredictionStyle = DefaultPredictionStyle; var historyFileName = hostName + "_history.txt"; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -309,6 +322,11 @@ public object ContinuationPromptColor public string HistorySavePath { get; set; } public HistorySaveStyle HistorySaveStyle { get; set; } + /// + /// How the predictive suggestion is rendered. + /// + public PredictionStyle PredictionStyle { get; set; } + /// /// How long in milliseconds should we wait before concluding /// the input is not an escape sequence? @@ -407,6 +425,12 @@ public object SelectionColor set => _selectionColor = VTColorUtils.AsEscapeSequence(value); } + public object PredictionColor + { + get => _predictionColor; + set => _predictionColor = VTColorUtils.AsEscapeSequence(value); + } + internal string _defaultTokenColor; internal string _commentColor; internal string _keywordColor; @@ -421,6 +445,7 @@ public object SelectionColor internal string _emphasisColor; internal string _errorColor; internal string _selectionColor; + internal string _predictionColor; internal void ResetColors() { @@ -438,6 +463,7 @@ internal void ResetColors() MemberColor = DefaultNumberColor; EmphasisColor = DefaultEmphasisColor; ErrorColor = DefaultErrorColor; + PredictionColor = DefaultPredictionColor; var bg = Console.BackgroundColor; if (fg == VTColorUtils.UnknownColor || bg == VTColorUtils.UnknownColor) @@ -473,7 +499,8 @@ internal void SetColor(string property, object value) {"Type", (o, v) => o.TypeColor = v}, {"Number", (o, v) => o.NumberColor = v}, {"Member", (o, v) => o.MemberColor = v}, - {"Selection", (o, v) => o.SelectionColor = v }, + {"Selection", (o, v) => o.SelectionColor = v}, + {"Prediction", (o, v) => o.PredictionColor = v}, }; Interlocked.CompareExchange(ref ColorSetters, setters, null); @@ -680,6 +707,14 @@ public ViModeStyle ViModeIndicator [Parameter] public ScriptBlock ViModeChangeHandler { get; set; } + [Parameter] + public PredictionStyle PredictionStyle + { + get => _predictionStyle.GetValueOrDefault(); + set => _predictionStyle = value; + } + internal PredictionStyle? _predictionStyle; + [Parameter] public Hashtable Colors { get; set; } diff --git a/PSReadLine/Completion.cs b/PSReadLine/Completion.cs index 22f884e05..908e34506 100644 --- a/PSReadLine/Completion.cs +++ b/PSReadLine/Completion.cs @@ -217,6 +217,9 @@ private void CompleteImpl(bool menuSelect) ViInsertWithAppend(); } + // Do not show suggestion text during tab completion. + using var _ = PredictionOff(); + var completions = GetCompletions(); if (completions == null || completions.CompletionMatches.Count == 0) return; diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index 6e0bd614c..c6b42762b 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -185,13 +185,14 @@ private string MaybeAddToHistory( bool fromDifferentSession = false, bool fromInitialRead = false) { - var addToHistoryOption = GetAddToHistoryOption(result); + var cmdLine = result.TrimEnd(); + var addToHistoryOption = GetAddToHistoryOption(cmdLine); if (addToHistoryOption != AddToHistoryOption.SkipAdding) { var fromHistoryFile = fromDifferentSession || fromInitialRead; _previousHistoryItem = new HistoryItem { - CommandLine = result, + CommandLine = cmdLine, _edits = edits, _undoEditIndex = undoEditIndex, _saved = fromHistoryFile, @@ -502,6 +503,8 @@ private void UpdateFromHistory(HistoryMoveCursor moveCursor) } break; } + + using var _ = PredictionOff(); Render(); } @@ -677,6 +680,20 @@ private void HistorySearch(int direction) } } + private string GetHistorySuggestion(string text) + { + for (int index = _history.Count - 1; index >= 0; index --) + { + var line = _history[index].CommandLine; + if (line.Length > text.Length && !LineIsMultiLine(line) && line.StartsWith(text, Options.HistoryStringComparison)) + { + return line; + } + } + + return null; + } + /// /// Move to the first item in the history. /// @@ -889,6 +906,7 @@ private void InteractiveHistorySearchLoop(int direction) private void InteractiveHistorySearch(int direction) { + using var _ = PredictionOff(); SaveCurrentLine(); // Add a status line that will contain the search prompt and string diff --git a/PSReadLine/Movement.cs b/PSReadLine/Movement.cs index 138191969..9d626d796 100644 --- a/PSReadLine/Movement.cs +++ b/PSReadLine/Movement.cs @@ -61,7 +61,14 @@ public static void ForwardChar(ConsoleKeyInfo? key = null, object arg = null) { if (TryGetArgAsInt(arg, out var numericArg, 1)) { - SetCursorPosition(_singleton._current + numericArg); + if (_singleton._current == _singleton._buffer.Length && numericArg > 0) + { + AcceptSuggestion(); + } + else + { + SetCursorPosition(_singleton._current + numericArg); + } } } @@ -311,6 +318,12 @@ public static void ForwardWord(ConsoleKeyInfo? key = null, object arg = null) return; } + if (_singleton._current == _singleton._buffer.Length && numericArg > 0) + { + AcceptNextSuggestionWord(numericArg); + return; + } + if (numericArg < 0) { BackwardWord(key, -numericArg); diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index fe45cf8f2..b5b3eb9a5 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -135,6 +135,10 @@ private void SetOptionsInternal(SetPSReadLineOption options) { Options.PromptText = options.PromptText; } + if (options._predictionStyle.HasValue) + { + Options.PredictionStyle = options.PredictionStyle; + } if (options.Colors != null) { IDictionaryEnumerator e = options.Colors.GetEnumerator(); diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index f55085310..971631cf8 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -4,11 +4,12 @@ Library Microsoft.PowerShell.PSReadLine Microsoft.PowerShell.PSReadLine2 - 2.0.1.0 - 2.0.1 - 2.0.1 + 2.1.0.0 + 2.1.0 + 2.1.0-beta1 true net461;netcoreapp2.1 + 8.0 diff --git a/PSReadLine/PSReadLine.format.ps1xml b/PSReadLine/PSReadLine.format.ps1xml index 0f437fa74..bac68ca61 100644 --- a/PSReadLine/PSReadLine.format.ps1xml +++ b/PSReadLine/PSReadLine.format.ps1xml @@ -158,6 +158,9 @@ $d = [Microsoft.PowerShell.KeyHandler]::GetGroupingDescription($_.Group) AnsiEscapeTimeout + + PredictionStyle + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.CommandColor) @@ -202,6 +205,10 @@ $d = [Microsoft.PowerShell.KeyHandler]::GetGroupingDescription($_.Group) [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.ParameterColor) + + + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.PredictionColor) + [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.SelectionColor) diff --git a/PSReadLine/PublicAPI.cs b/PSReadLine/PublicAPI.cs index 8cc457005..601794503 100644 --- a/PSReadLine/PublicAPI.cs +++ b/PSReadLine/PublicAPI.cs @@ -235,5 +235,34 @@ public static bool TryGetArgAsInt(object arg, out int numericArg, int defaultNum numericArg = 0; return false; } + + /// + /// Accept the suggestion text if there is one. + /// + public static void AcceptSuggestion() + { + if (_singleton._suggestionText != null) + { + using var _ = _singleton.PredictionOff(); + Replace(0, _singleton._buffer.Length, _singleton._suggestionText); + } + } + + /// + /// Accept the current or the next suggestion text word. + /// + public static void AcceptNextSuggestionWord(int numericArg) + { + if (_singleton._suggestionText != null) + { + int index = _singleton._buffer.Length; + while (numericArg-- > 0 && index < _singleton._suggestionText.Length) + { + index = _singleton.FindForwardSuggestionWordPoint(index, _singleton.Options.WordDelimiters); + } + + Replace(0, _singleton._buffer.Length, _singleton._suggestionText.Substring(0, index)); + } + } } } diff --git a/PSReadLine/ReadLine.vi.cs b/PSReadLine/ReadLine.vi.cs index ef7fe7e0d..ad48e19f4 100644 --- a/PSReadLine/ReadLine.vi.cs +++ b/PSReadLine/ReadLine.vi.cs @@ -522,6 +522,9 @@ internal static IDisposable UseViInsertModeTables() private void ViIndicateCommandMode() { + // Show suggestion in 'InsertMode' but not 'CommandMode'. + _showSuggestion = false; + if (_options.ViModeIndicator == ViModeStyle.Cursor) { _console.CursorSize = _normalCursorSize < 50 ? 100 : 25; @@ -541,6 +544,9 @@ private void ViIndicateCommandMode() private void ViIndicateInsertMode() { + // Show suggestion in 'InsertMode' but not 'CommandMode'. + _showSuggestion = true; + if (_options.ViModeIndicator == ViModeStyle.Cursor) { _console.CursorSize = _normalCursorSize; diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index 4a876b2dc..f476d5d2a 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -156,6 +156,7 @@ private void ForceRender() private int GenerateRender(string defaultColor) { var text = ParseInput(); + var suggestion = GetSuggestion(text); string color = defaultColor; string activeColor = string.Empty; @@ -297,7 +298,7 @@ void MaybeEmphasize(int i, string currColor) } currentLogicalLine += 1; - if (currentLogicalLine > _consoleBufferLines.Count - 1) + if (currentLogicalLine == _consoleBufferLines.Count) { _consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); } @@ -310,10 +311,7 @@ void MaybeEmphasize(int i, string currColor) activeColor = string.Empty; UpdateColorsIfNecessary(Options._continuationPromptColor); - foreach (char c in Options.ContinuationPrompt) - { - _consoleBufferLines[currentLogicalLine].Append(c); - } + _consoleBufferLines[currentLogicalLine].Append(Options.ContinuationPrompt); if (inSelectedRegion) { @@ -321,27 +319,53 @@ void MaybeEmphasize(int i, string currColor) _consoleBufferLines[currentLogicalLine].Append(Options.SelectionColor); } } + else if (char.IsControl(charToRender)) + { + MaybeEmphasize(i, color); + _consoleBufferLines[currentLogicalLine].Append('^'); + _consoleBufferLines[currentLogicalLine].Append((char)('@' + charToRender)); + } else { - if (char.IsControl(charToRender)) + MaybeEmphasize(i, color); + _consoleBufferLines[currentLogicalLine].Append(charToRender); + } + } + + if (inSelectedRegion) + { + _consoleBufferLines[currentLogicalLine].Append("\x1b[0m"); + } + + inSelectedRegion = false; + if (suggestion != null) + { + color = _options._predictionColor; + UpdateColorsIfNecessary(color); + + foreach (char charToRender in suggestion) + { + if (charToRender == '\n') + { + currentLogicalLine += 1; + if (currentLogicalLine == _consoleBufferLines.Count) + { + _consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); + } + _consoleBufferLines[currentLogicalLine].Append(Options.ContinuationPrompt); + } + else if (char.IsControl(charToRender)) { - MaybeEmphasize(i, color); _consoleBufferLines[currentLogicalLine].Append('^'); _consoleBufferLines[currentLogicalLine].Append((char)('@' + charToRender)); } else { - MaybeEmphasize(i, color); _consoleBufferLines[currentLogicalLine].Append(charToRender); } } } - if (inSelectedRegion) - { - _consoleBufferLines[currentLogicalLine].Append("\x1b[0m"); - } - if (_statusLinePrompt != null) { currentLogicalLine += 1; @@ -1194,6 +1218,16 @@ private bool LineIsMultiLine() return false; } + private bool LineIsMultiLine(string text) + { + for (int i = 0; i < text.Length; i++) + { + if (text[i] == '\n') + return true; + } + return false; + } + private int GetStatusLineCount() { if (_statusLinePrompt == null) diff --git a/PSReadLine/Suggestion.cs b/PSReadLine/Suggestion.cs new file mode 100644 index 000000000..f660b460c --- /dev/null +++ b/PSReadLine/Suggestion.cs @@ -0,0 +1,61 @@ +using System; + +namespace Microsoft.PowerShell +{ + public partial class PSConsoleReadLine + { + private class SuggestionModeRestore : IDisposable + { + private bool oldSuggestionMode; + + internal SuggestionModeRestore(bool showSuggestion) + { + oldSuggestionMode = _singleton._showSuggestion; + _singleton._showSuggestion = showSuggestion; + } + + public void Dispose() + { + _singleton._showSuggestion = oldSuggestionMode; + } + } + + private string _suggestionText; + private uint _lastRenderedTextHash; + private bool _showSuggestion = true; + + /// + /// Turn off the prediction feature for the scope of the calling method. + /// Note: the calling method need to use this method with the 'using' statement/variable, + /// so that the suggestion feature can be properly restored. + /// + private SuggestionModeRestore PredictionOff() => new SuggestionModeRestore(false); + + private string GetSuggestion(string text) + { + if (_options.PredictionStyle == PredictionStyle.None || !_showSuggestion || + LineIsMultiLine() || string.IsNullOrWhiteSpace(text)) + { + _suggestionText = null; + _lastRenderedTextHash = 0; + return null; + } + + try + { + uint textHash = _lastRenderedTextHash; + _lastRenderedTextHash = FNV1a32Hash.ComputeHash(text); + if (textHash != _lastRenderedTextHash) + { + _suggestionText = GetHistorySuggestion(text); + } + } + catch + { + _suggestionText = null; + } + + return _suggestionText?.Substring(text.Length); + } + } +} diff --git a/PSReadLine/Words.cs b/PSReadLine/Words.cs index a9ea20289..2888b9246 100644 --- a/PSReadLine/Words.cs +++ b/PSReadLine/Words.cs @@ -94,6 +94,57 @@ private bool InWord(int index, string wordDelimiters) return !char.IsWhiteSpace(c) && wordDelimiters.IndexOf(c) < 0; } + private bool InWord(string text, int index, string wordDelimiters) + { + char c = text[index]; + return !char.IsWhiteSpace(c) && wordDelimiters.IndexOf(c) < 0; + } + + private int FindForwardSuggestionWordPoint(int currentIndex, string wordDelimiters) + { + System.Diagnostics.Debug.Assert( + _suggestionText != null && _suggestionText.Length > _buffer.Length, + "Caller needs to make srue the suggestion text exist."); + + if (currentIndex >= _suggestionText.Length) + { + return _suggestionText.Length; + } + + int i = currentIndex; + if (!InWord(_suggestionText, i, wordDelimiters)) + { + // Scan to end of current non-word region + while (++i < _suggestionText.Length) + { + if (InWord(_suggestionText, i, wordDelimiters)) + { + break; + } + } + } + + if (i < _suggestionText.Length) + { + while (++i < _suggestionText.Length) + { + if (!InWord(_suggestionText, i, wordDelimiters)) + { + if (_suggestionText[i] == ' ') + { + // If the end of this suggestion word is a space, then we consider the word + // is complete and include the space in the accepted suggestion word. + i++; + } + + break; + } + } + } + + return i; + } + /// /// Find the end of the current/next word as defined by wordDelimiters and whitespace. /// diff --git a/docs/Set-PSReadLineOption.md b/docs/Set-PSReadLineOption.md index 09c7f2ede..0aa446276 100644 --- a/docs/Set-PSReadLineOption.md +++ b/docs/Set-PSReadLineOption.md @@ -36,6 +36,7 @@ Set-PSReadLineOption [-AnsiEscapeTimeout ] [-ViModeIndicator ] [-ViModeChangeHandler ] + [-PredictionStyle ] ``` ## DESCRIPTION @@ -389,6 +390,8 @@ The valid keys include: -- Member: The member name token color. +-- Prediction: The color for the suggestion text that comes from the prediction API. + ```yaml Type: Hashtable Parameter Sets: (All) @@ -612,6 +615,28 @@ Accept pipeline input: false Accept wildcard characters: false ``` +### -PredictionStyle + +Specifies how PSReadLine should render the predictive suggestions. + +Valid values are: + +-- None: disable the predictive suggestion feature + +-- Concise: render the predictive suggestion in a concise view + +```yaml +Type: PredictionStyle +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: Concise +Accept pipeline input: false +Accept wildcard characters: false +``` + ## INPUTS ### None diff --git a/test/SuggestionTest.cs b/test/SuggestionTest.cs new file mode 100644 index 000000000..088ec1929 --- /dev/null +++ b/test/SuggestionTest.cs @@ -0,0 +1,110 @@ +using Microsoft.PowerShell; +using System; +using System.Collections; +using Xunit; + +namespace Test +{ + public partial class ReadLine + { + [SkippableFact] + public void RenderSuggestion() + { + TestSetup(KeyMode.Cmd, + new KeyHandler("Ctrl+f", PSConsoleReadLine.ForwardWord)); + + // No matching history entry + SetHistory("echo -bar", "eca -zoo"); + Test("a", Keys( + 'a', CheckThat(() => AssertScreenIs(1, TokenClassification.Command, 'a')), + _.Ctrl_f, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, 'a')), + _.RightArrow, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, 'a')), + CheckThat(() => AssertCursorLeftIs(1)))); + + // Different matches as more input coming + SetHistory("echo -bar", "eca -zoo"); + Test("ech", Keys( + 'e', CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, 'e', + TokenClassification.Prediction, "ca -zoo")), + 'c', CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "ec", + TokenClassification.Prediction, "a -zoo")), + 'h', CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "ech", + TokenClassification.Prediction, "o -bar")), + // Once accepted, the suggestion text should be blanked out. + _.Enter, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "ech")) + )); + + // Accept all or partial suggestion text with 'ForwardChar' and 'ForwardWord' + SetHistory("echo -bar", "eca -zoo"); + Test("eca ", Keys( + 'e', CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, 'e', + TokenClassification.Prediction, "ca -zoo")), + _.RightArrow, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "eca", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo")), + _.Ctrl_z, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, 'e', + TokenClassification.Prediction, "ca -zoo")), + _.Ctrl_f, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "eca", + TokenClassification.None, ' ', + TokenClassification.Prediction, "-zoo")), + CheckThat(() => AssertCursorLeftIs(4)), + _.Ctrl_f, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "eca", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo")), + _.Ctrl_z, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "eca", + TokenClassification.None, ' ', + TokenClassification.Prediction, "-zoo")) + )); + } + + [SkippableFact] + public void DisablePrediction() + { + TestSetup(KeyMode.Cmd); + SetHistory("echo -bar", "eca -zoo"); + PSConsoleReadLine.SetOptions(new SetPSReadLineOption {PredictionStyle = PredictionStyle.None}); + + Test("ech", Keys( + 'e', CheckThat(() => AssertScreenIs(1, TokenClassification.Command, 'e')), + 'c', CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "ec")), + 'h', CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "ech")), + _.Enter, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "ech")) + )); + } + + [SkippableFact] + public void SetPredictionColor() + { + TestSetup(KeyMode.Cmd); + var predictionColor = MakeCombinedColor(ConsoleColor.DarkYellow, ConsoleColor.Yellow); + var predictionColorToCheck = Tuple.Create(ConsoleColor.DarkYellow, ConsoleColor.Yellow); + PSConsoleReadLine.SetOptions(new SetPSReadLineOption {Colors = new Hashtable(){{"Prediction", predictionColor}}}); + + SetHistory("echo -bar", "eca -zoo"); + Test("ech", Keys( + 'e', CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, 'e', + predictionColorToCheck, "ca -zoo")), + 'c', CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "ec", + predictionColorToCheck, "a -zoo")), + 'h', CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "ech", + predictionColorToCheck, "o -bar")), + // Once accepted, the suggestion text should be blanked out. + _.Enter, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "ech")) + )); + } + } +} diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index b17e3345a..ef5e6554b 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -50,6 +50,7 @@ public enum TokenClassification Number, Member, Selection, + Prediction, } public abstract partial class ReadLine @@ -101,6 +102,7 @@ private enum KeyMode /*Number*/ ConsoleColor.DarkBlue, /*Member*/ ConsoleColor.DarkMagenta, /*Selection*/ ConsoleColor.Black, + /*Prediction*/ConsoleColor.DarkGreen, }; internal static readonly ConsoleColor[] BackgroundColors = new[] @@ -116,7 +118,8 @@ private enum KeyMode /*Type*/ ConsoleColor.Black, /*Number*/ ConsoleColor.Gray, /*Member*/ ConsoleColor.Yellow, - /*Selection*/ ConsoleColor.Gray + /*Selection*/ ConsoleColor.Gray, + /*Prediction*/ConsoleColor.Cyan, }; class KeyHandler @@ -475,6 +478,7 @@ private void TestSetup(KeyMode keyMode, params KeyHandler[] keyHandlers) ShowToolTips = PSConsoleReadLineOptions.DefaultShowToolTips, WordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters, PromptText = new [] {""}, + PredictionStyle = PredictionStyle.Concise, Colors = new Hashtable { { "ContinuationPrompt", MakeCombinedColor(_console.ForegroundColor, _console.BackgroundColor) }, { "Emphasis", MakeCombinedColor(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor) }, @@ -505,7 +509,7 @@ private void TestSetup(KeyMode keyMode, params KeyHandler[] keyHandlers) var tokenTypes = new[] { "Default", "Comment", "Keyword", "String", "Operator", "Variable", - "Command", "Parameter", "Type", "Number", "Member", "Selection" + "Command", "Parameter", "Type", "Number", "Member", "Selection", "Prediction" }; var colors = new Hashtable(); for (var i = 0; i < tokenTypes.Length; i++) From e876ffcd323e8dd2d4fa8e543b1635e60cfd79ee Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 1 May 2020 09:55:02 -0700 Subject: [PATCH 2/6] Fix code that caused 'ArgumentOutOfRangeException' during undoing edits --- PSReadLine/History.cs | 10 +++++----- test/SuggestionTest.cs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index c6b42762b..bc4f8f5f0 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -185,14 +185,13 @@ private string MaybeAddToHistory( bool fromDifferentSession = false, bool fromInitialRead = false) { - var cmdLine = result.TrimEnd(); - var addToHistoryOption = GetAddToHistoryOption(cmdLine); + var addToHistoryOption = GetAddToHistoryOption(result); if (addToHistoryOption != AddToHistoryOption.SkipAdding) { var fromHistoryFile = fromDifferentSession || fromInitialRead; _previousHistoryItem = new HistoryItem { - CommandLine = cmdLine, + CommandLine = result, _edits = edits, _undoEditIndex = undoEditIndex, _saved = fromHistoryFile, @@ -684,8 +683,9 @@ private string GetHistorySuggestion(string text) { for (int index = _history.Count - 1; index >= 0; index --) { - var line = _history[index].CommandLine; - if (line.Length > text.Length && !LineIsMultiLine(line) && line.StartsWith(text, Options.HistoryStringComparison)) + var line = _history[index].CommandLine.TrimEnd(); + if (line.Length > text.Length && !LineIsMultiLine(line) && + line.StartsWith(text, Options.HistoryStringComparison)) { return line; } diff --git a/test/SuggestionTest.cs b/test/SuggestionTest.cs index 088ec1929..d94bc6293 100644 --- a/test/SuggestionTest.cs +++ b/test/SuggestionTest.cs @@ -106,5 +106,36 @@ public void SetPredictionColor() TokenClassification.Command, "ech")) )); } + + [SkippableFact] + public void HistoryEditsCanUndoProperly() + { + TestSetup(KeyMode.Cmd, + new KeyHandler("Ctrl+f", PSConsoleReadLine.ForwardWord)); + SetHistory("git checkout -b branch origin/bbbb"); + + // Accept partial suggestion. + Test("git checkout ", Keys( + "git ch", CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "git", + TokenClassification.None, " ch", + TokenClassification.Prediction, "eckout -b branch origin/bbbb")), + _.Ctrl_f, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "git", + TokenClassification.None, " checkout ", + TokenClassification.Prediction, "-b branch origin/bbbb")), + _.Enter, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "git", + TokenClassification.None, " checkout ")) + )); + + // Get the last command line from history, and revert the line. + // 'RevertLine' will undo all edits of the history command. + Test("", Keys( + _.UpArrow, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "git", + TokenClassification.None, " checkout ")), + _.Escape)); + } } } From cc5f1a333e8ed9f3fddc9d0959cb26ef7f27b1f5 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 7 May 2020 16:35:47 -0700 Subject: [PATCH 3/6] Address comments --- PSReadLine/BasicEditing.cs | 2 +- PSReadLine/Changes.txt | 19 +--- PSReadLine/Cmdlets.cs | 42 +++++++-- PSReadLine/History.cs | 9 +- PSReadLine/KeyBindings.cs | 2 + PSReadLine/Options.cs | 8 +- PSReadLine/PSReadLine.format.ps1xml | 5 +- PSReadLine/PSReadLineResources.resx | 6 ++ PSReadLine/PublicAPI.cs | 29 ------ PSReadLine/Render.cs | 112 ++++++++++------------- PSReadLine/Suggestion.cs | 108 +++++++++++++++++----- PSReadLine/Words.cs | 2 +- docs/Set-PSReadLineOption.md | 30 +++++- test/SuggestionTest.cs | 136 ++++++++++++++++++++++++++-- test/UnitTestReadLine.cs | 2 +- 15 files changed, 343 insertions(+), 169 deletions(-) diff --git a/PSReadLine/BasicEditing.cs b/PSReadLine/BasicEditing.cs index 492e59b68..70c95dd57 100644 --- a/PSReadLine/BasicEditing.cs +++ b/PSReadLine/BasicEditing.cs @@ -274,7 +274,7 @@ private bool AcceptLineImpl(bool validate) SetCursorPosition(_current); if (_suggestionText != null) { - _suggestionText = null; + ResetSuggestion(); _console.BlankRestOfLine(); } diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt index 637f5c875..31c5c2f48 100644 --- a/PSReadLine/Changes.txt +++ b/PSReadLine/Changes.txt @@ -2,24 +2,7 @@ Pre-release notes: -#### Prototype to support the fish-like auto-suggestion in PSReadLine - -Currently: - -- The only suggestion source is the PSReadLine history file. - The ultimate goal is to call into a prediction API in PowerShell engine that is powered by a prediction plugin. -- By default, the suggestion text is rendered with the `dark black` color `\x1b[38;5;238m`, which works OK with a black background. - You can change it by running `Set-PSReadLineOption -Colors @{ Prediction = '' }`. -- VI mode - now the suggestion is disabled when switching to the command mode, and enabled when switching to insert mode. -- Very limited efforts have been spent on the VI/Emacs experience, so it could be pretty raw for them, and feedback is very welcome. - -When using this PSReadLine, please add the following key binding to your profile: - - Set-PSReadLineKeyHandler -Key "Ctrl+f" -Function ForwardWord - -- Pressing `RightArrow` at the end of current typing will accept the suggestion text -- Pressing `Ctrl+f` at the end of current typing will accept a word from the suggestion text -- Pressing `Ctrl+z` will get you back to where you were before accepting all or part of the suggestion +* Experimental support for fish-like suggestions in PSReadLine. ### Version 2.0.1 diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index 7fe7d7382..95e038efe 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -63,10 +63,15 @@ public enum AddToHistoryOption MemoryAndFile } - public enum PredictionStyle + public enum PredictionSource { None, - Concise, + History, + } + + public enum PredictionViewStyle + { + Default, } public class PSConsoleReadLineOptions @@ -138,7 +143,12 @@ public class PSConsoleReadLineOptions public const HistorySaveStyle DefaultHistorySaveStyle = HistorySaveStyle.SaveIncrementally; - public const PredictionStyle DefaultPredictionStyle = PredictionStyle.Concise; + /// + /// The predictive suggestion feature is disabled by default. + /// + public const PredictionSource DefaultPredictionSource = PredictionSource.None; + + public const PredictionViewStyle DefaultPredictionViewStyle = PredictionViewStyle.Default; /// /// How long in milliseconds should we wait before concluding @@ -167,7 +177,8 @@ public PSConsoleReadLineOptions(string hostName) HistorySearchCaseSensitive = DefaultHistorySearchCaseSensitive; HistorySaveStyle = DefaultHistorySaveStyle; AnsiEscapeTimeout = DefaultAnsiEscapeTimeout; - PredictionStyle = DefaultPredictionStyle; + PredictionSource = DefaultPredictionSource; + PredictionViewStyle = DefaultPredictionViewStyle; var historyFileName = hostName + "_history.txt"; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -322,10 +333,15 @@ public object ContinuationPromptColor public string HistorySavePath { get; set; } public HistorySaveStyle HistorySaveStyle { get; set; } + /// + /// Sets the source to get predictive suggestions. + /// + public PredictionSource PredictionSource { get; set; } + /// /// How the predictive suggestion is rendered. /// - public PredictionStyle PredictionStyle { get; set; } + public PredictionViewStyle PredictionViewStyle { get; set; } /// /// How long in milliseconds should we wait before concluding @@ -708,12 +724,20 @@ public ViModeStyle ViModeIndicator public ScriptBlock ViModeChangeHandler { get; set; } [Parameter] - public PredictionStyle PredictionStyle + public PredictionSource PredictionSource + { + get => _predictionSource.GetValueOrDefault(); + set => _predictionSource = value; + } + internal PredictionSource? _predictionSource; + + [Parameter] + public PredictionViewStyle PredictionViewStyle { - get => _predictionStyle.GetValueOrDefault(); - set => _predictionStyle = value; + get => _predictionViewStyle.GetValueOrDefault(); + set => _predictionViewStyle = value; } - internal PredictionStyle? _predictionStyle; + internal PredictionViewStyle? _predictionViewStyle; [Parameter] public Hashtable Colors { get; set; } diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index bc4f8f5f0..1fc4d6f91 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -679,13 +679,18 @@ private void HistorySearch(int direction) } } + /// + /// Currently we only select single-line history that is prefixed with the user input, + /// but it can be improved to not strictly use the user input as a prefix, but a hint + /// to extract a partial pipeline or statement from a single-line or multiple-line + /// history entry. + /// private string GetHistorySuggestion(string text) { for (int index = _history.Count - 1; index >= 0; index --) { var line = _history[index].CommandLine.TrimEnd(); - if (line.Length > text.Length && !LineIsMultiLine(line) && - line.StartsWith(text, Options.HistoryStringComparison)) + if (line.Length > text.Length && !LineIsMultiLine(line) && line.StartsWith(text, Options.HistoryStringComparison)) { return line; } diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index 01491540a..b60b018db 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -549,6 +549,8 @@ public static KeyHandlerGroup GetDisplayGrouping(string function) case nameof(ViExit): case nameof(ViInsertMode): case nameof(WhatIsKey): + case nameof(AcceptSuggestion): + case nameof(AcceptNextSuggestionWord): return KeyHandlerGroup.Miscellaneous; case nameof(CharacterSearch): diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index b5b3eb9a5..dabc5bfbe 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -135,9 +135,13 @@ private void SetOptionsInternal(SetPSReadLineOption options) { Options.PromptText = options.PromptText; } - if (options._predictionStyle.HasValue) + if (options._predictionSource.HasValue) { - Options.PredictionStyle = options.PredictionStyle; + Options.PredictionSource = options.PredictionSource; + } + if (options._predictionViewStyle.HasValue) + { + Options.PredictionViewStyle = options.PredictionViewStyle; } if (options.Colors != null) { diff --git a/PSReadLine/PSReadLine.format.ps1xml b/PSReadLine/PSReadLine.format.ps1xml index bac68ca61..306d09269 100644 --- a/PSReadLine/PSReadLine.format.ps1xml +++ b/PSReadLine/PSReadLine.format.ps1xml @@ -159,7 +159,10 @@ $d = [Microsoft.PowerShell.KeyHandler]::GetGroupingDescription($_.Group) AnsiEscapeTimeout - PredictionStyle + PredictionSource + + + PredictionViewStyle diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index fd9def428..0f5ae7810 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -741,6 +741,12 @@ If there are other parse errors, unresolved commands, or incorrect parameters, s Inserts a new empty line below the current line without attempting to execute the input + + Accept the current inline or selected suggestion + + + Accept the next word of the inline or selected suggestion + Error reading or writing history file '{0}': {1} diff --git a/PSReadLine/PublicAPI.cs b/PSReadLine/PublicAPI.cs index 601794503..8cc457005 100644 --- a/PSReadLine/PublicAPI.cs +++ b/PSReadLine/PublicAPI.cs @@ -235,34 +235,5 @@ public static bool TryGetArgAsInt(object arg, out int numericArg, int defaultNum numericArg = 0; return false; } - - /// - /// Accept the suggestion text if there is one. - /// - public static void AcceptSuggestion() - { - if (_singleton._suggestionText != null) - { - using var _ = _singleton.PredictionOff(); - Replace(0, _singleton._buffer.Length, _singleton._suggestionText); - } - } - - /// - /// Accept the current or the next suggestion text word. - /// - public static void AcceptNextSuggestionWord(int numericArg) - { - if (_singleton._suggestionText != null) - { - int index = _singleton._buffer.Length; - while (numericArg-- > 0 && index < _singleton._suggestionText.Length) - { - index = _singleton.FindForwardSuggestionWordPoint(index, _singleton.Options.WordDelimiters); - } - - Replace(0, _singleton._buffer.Length, _singleton._suggestionText.Substring(0, index)); - } - } } } diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index f476d5d2a..83ad17b87 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -173,13 +173,52 @@ void UpdateColorsIfNecessary(string newColor) } } - void MaybeEmphasize(int i, string currColor) + void RenderOneChar(char charToRender, bool toEmphasize) { - if (i >= _emphasisStart && i < (_emphasisStart + _emphasisLength)) + if (charToRender == '\n') + { + if (inSelectedRegion) + { + // Turn off inverse before end of line, turn on after continuation prompt + _consoleBufferLines[currentLogicalLine].Append("\x1b[0m"); + } + + currentLogicalLine += 1; + if (currentLogicalLine == _consoleBufferLines.Count) + { + _consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); + } + + // Reset the color for continuation prompt so the color sequence will always be explicitly + // specified for continuation prompt in the generated render strings. + // This is necessary because we will likely not rewrite all texts during rendering, and thus + // we cannot assume the continuation prompt can continue to use the active color setting from + // the previous rendering string. + activeColor = string.Empty; + + UpdateColorsIfNecessary(Options._continuationPromptColor); + _consoleBufferLines[currentLogicalLine].Append(Options.ContinuationPrompt); + + if (inSelectedRegion) + { + // Turn off inverse before end of line, turn on after continuation prompt + _consoleBufferLines[currentLogicalLine].Append(Options.SelectionColor); + } + + return; + } + + UpdateColorsIfNecessary(toEmphasize ? _options._emphasisColor : color); + + if (char.IsControl(charToRender)) { - currColor = _options._emphasisColor; + _consoleBufferLines[currentLogicalLine].Append('^'); + _consoleBufferLines[currentLogicalLine].Append((char)('@' + charToRender)); + } + else + { + _consoleBufferLines[currentLogicalLine].Append(charToRender); } - UpdateColorsIfNecessary(currColor); } foreach (var buf in _consoleBufferLines) @@ -289,80 +328,23 @@ void MaybeEmphasize(int i, string currColor) } var charToRender = text[i]; - if (charToRender == '\n') - { - if (inSelectedRegion) - { - // Turn off inverse before end of line, turn on after continuation prompt - _consoleBufferLines[currentLogicalLine].Append("\x1b[0m"); - } - - currentLogicalLine += 1; - if (currentLogicalLine == _consoleBufferLines.Count) - { - _consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); - } - - // Reset the color for continuation prompt so the color sequence will always be explicitly - // specified for continuation prompt in the generated render strings. - // This is necessary because we will likely not rewrite all texts during rendering, and thus - // we cannot assume the continuation prompt can continue to use the active color setting from - // the previous rendering string. - activeColor = string.Empty; + var toEmphasize = i >= _emphasisStart && i < (_emphasisStart + _emphasisLength); - UpdateColorsIfNecessary(Options._continuationPromptColor); - _consoleBufferLines[currentLogicalLine].Append(Options.ContinuationPrompt); - - if (inSelectedRegion) - { - // Turn off inverse before end of line, turn on after continuation prompt - _consoleBufferLines[currentLogicalLine].Append(Options.SelectionColor); - } - } - else if (char.IsControl(charToRender)) - { - MaybeEmphasize(i, color); - _consoleBufferLines[currentLogicalLine].Append('^'); - _consoleBufferLines[currentLogicalLine].Append((char)('@' + charToRender)); - } - else - { - MaybeEmphasize(i, color); - _consoleBufferLines[currentLogicalLine].Append(charToRender); - } + RenderOneChar(charToRender, toEmphasize); } if (inSelectedRegion) { _consoleBufferLines[currentLogicalLine].Append("\x1b[0m"); + inSelectedRegion = false; } - inSelectedRegion = false; if (suggestion != null) { color = _options._predictionColor; - UpdateColorsIfNecessary(color); - foreach (char charToRender in suggestion) { - if (charToRender == '\n') - { - currentLogicalLine += 1; - if (currentLogicalLine == _consoleBufferLines.Count) - { - _consoleBufferLines.Add(new StringBuilder(COMMON_WIDEST_CONSOLE_WIDTH)); - } - _consoleBufferLines[currentLogicalLine].Append(Options.ContinuationPrompt); - } - else if (char.IsControl(charToRender)) - { - _consoleBufferLines[currentLogicalLine].Append('^'); - _consoleBufferLines[currentLogicalLine].Append((char)('@' + charToRender)); - } - else - { - _consoleBufferLines[currentLogicalLine].Append(charToRender); - } + RenderOneChar(charToRender, toEmphasize: false); } } diff --git a/PSReadLine/Suggestion.cs b/PSReadLine/Suggestion.cs index f660b460c..6ae513979 100644 --- a/PSReadLine/Suggestion.cs +++ b/PSReadLine/Suggestion.cs @@ -4,49 +4,46 @@ namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { - private class SuggestionModeRestore : IDisposable - { - private bool oldSuggestionMode; - - internal SuggestionModeRestore(bool showSuggestion) - { - oldSuggestionMode = _singleton._showSuggestion; - _singleton._showSuggestion = showSuggestion; - } - - public void Dispose() - { - _singleton._showSuggestion = oldSuggestionMode; - } - } - private string _suggestionText; - private uint _lastRenderedTextHash; + private string _userInputText; private bool _showSuggestion = true; + private bool IsPredictionOn => _options.PredictionSource != PredictionSource.None && _showSuggestion; + private bool IsPredictionOff => _options.PredictionSource == PredictionSource.None || !_showSuggestion; + /// /// Turn off the prediction feature for the scope of the calling method. /// Note: the calling method need to use this method with the 'using' statement/variable, /// so that the suggestion feature can be properly restored. /// - private SuggestionModeRestore PredictionOff() => new SuggestionModeRestore(false); + private IDisposable PredictionOff() + { + var oldSuggestionMode = _singleton._showSuggestion; + _singleton._showSuggestion = false; + + return new Disposable(() => _singleton._showSuggestion = oldSuggestionMode); + } + + private void ResetSuggestion() + { + _suggestionText = null; + _userInputText = null; + } private string GetSuggestion(string text) { - if (_options.PredictionStyle == PredictionStyle.None || !_showSuggestion || - LineIsMultiLine() || string.IsNullOrWhiteSpace(text)) + if (IsPredictionOff || LineIsMultiLine() || string.IsNullOrWhiteSpace(text)) { - _suggestionText = null; - _lastRenderedTextHash = 0; + ResetSuggestion(); return null; } try { - uint textHash = _lastRenderedTextHash; - _lastRenderedTextHash = FNV1a32Hash.ComputeHash(text); - if (textHash != _lastRenderedTextHash) + if (_suggestionText == null || _suggestionText.Length <= text.Length || + text.Length < _userInputText.Length || !_suggestionText.StartsWith(text, _options.HistoryStringComparison)) { + _userInputText = text; _suggestionText = GetHistorySuggestion(text); } } @@ -57,5 +54,66 @@ private string GetSuggestion(string text) return _suggestionText?.Substring(text.Length); } + + /// + /// Accept the suggestion text if there is one. + /// + public static void AcceptSuggestion(ConsoleKeyInfo? key = null, object arg = null) + { + if (!TryGetArgAsInt(arg, out var numericArg, 1)) + { + return; + } + + if (numericArg > 0) + { + AcceptSuggestion(); + } + } + + private static void AcceptSuggestion() + { + if (_singleton._suggestionText != null) + { + // Ignore the visual selection. + _singleton._visualSelectionCommandCount = 0; + + using var _ = _singleton.PredictionOff(); + Replace(0, _singleton._buffer.Length, _singleton._suggestionText); + } + } + + /// + /// Accept the current or the next suggestion text word. + /// + public static void AcceptNextSuggestionWord(ConsoleKeyInfo? key = null, object arg = null) + { + if (!TryGetArgAsInt(arg, out var numericArg, 1)) + { + return; + } + + if (numericArg > 0) + { + AcceptNextSuggestionWord(numericArg); + } + } + + private static void AcceptNextSuggestionWord(int numericArg) + { + if (_singleton._suggestionText != null) + { + // Ignore the visual selection. + _singleton._visualSelectionCommandCount = 0; + + int index = _singleton._buffer.Length; + while (numericArg-- > 0 && index < _singleton._suggestionText.Length) + { + index = _singleton.FindForwardSuggestionWordPoint(index, _singleton.Options.WordDelimiters); + } + + Replace(0, _singleton._buffer.Length, _singleton._suggestionText.Substring(0, index)); + } + } } } diff --git a/PSReadLine/Words.cs b/PSReadLine/Words.cs index 2888b9246..4c6e014c5 100644 --- a/PSReadLine/Words.cs +++ b/PSReadLine/Words.cs @@ -104,7 +104,7 @@ private int FindForwardSuggestionWordPoint(int currentIndex, string wordDelimite { System.Diagnostics.Debug.Assert( _suggestionText != null && _suggestionText.Length > _buffer.Length, - "Caller needs to make srue the suggestion text exist."); + "Caller needs to make sure the suggestion text exist."); if (currentIndex >= _suggestionText.Length) { diff --git a/docs/Set-PSReadLineOption.md b/docs/Set-PSReadLineOption.md index 0aa446276..883ee5726 100644 --- a/docs/Set-PSReadLineOption.md +++ b/docs/Set-PSReadLineOption.md @@ -615,15 +615,35 @@ Accept pipeline input: false Accept wildcard characters: false ``` -### -PredictionStyle +### -PredictionSource -Specifies how PSReadLine should render the predictive suggestions. +Specifies the source for PSReadLine to get predictive suggestions. Valid values are: --- None: disable the predictive suggestion feature +-- None: disable the predictive suggestion feature. + +-- History: get predictive suggestions from history only. + +```yaml +Type: PredictionStyle +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: false +Accept wildcard characters: false +``` + +### -PredictionViewStyle + +Specifies how PSReadLine should render the predictive suggestions. + +Valid values are: --- Concise: render the predictive suggestion in a concise view +-- Default: Render the suggestion in an inline manner. ```yaml Type: PredictionStyle @@ -632,7 +652,7 @@ Aliases: Required: False Position: Named -Default value: Concise +Default value: Default Accept pipeline input: false Accept wildcard characters: false ``` diff --git a/test/SuggestionTest.cs b/test/SuggestionTest.cs index d94bc6293..34fdd87d3 100644 --- a/test/SuggestionTest.cs +++ b/test/SuggestionTest.cs @@ -24,13 +24,13 @@ public void RenderSuggestion() // Different matches as more input coming SetHistory("echo -bar", "eca -zoo"); Test("ech", Keys( - 'e', CheckThat(() => AssertScreenIs(1, + 'e', CheckThat(() => AssertScreenIs(1, TokenClassification.Command, 'e', TokenClassification.Prediction, "ca -zoo")), - 'c', CheckThat(() => AssertScreenIs(1, + 'c', CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "ec", TokenClassification.Prediction, "a -zoo")), - 'h', CheckThat(() => AssertScreenIs(1, + 'h', CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "ech", TokenClassification.Prediction, "o -bar")), // Once accepted, the suggestion text should be blanked out. @@ -41,38 +41,154 @@ public void RenderSuggestion() // Accept all or partial suggestion text with 'ForwardChar' and 'ForwardWord' SetHistory("echo -bar", "eca -zoo"); Test("eca ", Keys( - 'e', CheckThat(() => AssertScreenIs(1, + 'e', CheckThat(() => AssertScreenIs(1, TokenClassification.Command, 'e', TokenClassification.Prediction, "ca -zoo")), - _.RightArrow, CheckThat(() => AssertScreenIs(1, + _.RightArrow, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo")), - _.Ctrl_z, CheckThat(() => AssertScreenIs(1, + _.Ctrl_z, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, 'e', TokenClassification.Prediction, "ca -zoo")), - _.Ctrl_f, CheckThat(() => AssertScreenIs(1, + _.Ctrl_f, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Prediction, "-zoo")), CheckThat(() => AssertCursorLeftIs(4)), - _.Ctrl_f, CheckThat(() => AssertScreenIs(1, + _.Ctrl_f, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Parameter, "-zoo")), - _.Ctrl_z, CheckThat(() => AssertScreenIs(1, + _.Ctrl_z, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, "eca", TokenClassification.None, ' ', TokenClassification.Prediction, "-zoo")) )); } + [SkippableFact] + public void CustomKeyBindingsToAcceptSuggestion() + { + TestSetup(KeyMode.Cmd, + new KeyHandler("Alt+g", PSConsoleReadLine.AcceptSuggestion), + new KeyHandler("Alt+f", PSConsoleReadLine.AcceptNextSuggestionWord)); + + SetHistory("echo -bar", "eca -zoo"); + Test("eca ", Keys( + "ec", CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "ec", + TokenClassification.Prediction, "a -zoo")), + CheckThat(() => AssertCursorLeftIs(2)), + _.Alt_g, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "eca", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo")), + CheckThat(() => AssertCursorLeftIs(8)), + _.Ctrl_z, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "ec", + TokenClassification.Prediction, "a -zoo")), + CheckThat(() => AssertCursorLeftIs(2)), + _.Alt_f, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "eca", + TokenClassification.None, ' ', + TokenClassification.Prediction, "-zoo")), + CheckThat(() => AssertCursorLeftIs(4)), + _.Alt_f, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "eca", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo")), + CheckThat(() => AssertCursorLeftIs(8)), + _.Ctrl_z, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "eca", + TokenClassification.None, ' ', + TokenClassification.Prediction, "-zoo")), + CheckThat(() => AssertCursorLeftIs(4)), + + // Revert back to 'ec'. + _.Ctrl_z, CheckThat(() => AssertCursorLeftIs(2)), + // Move cursor to column 0. + _.LeftArrow, _.LeftArrow, + CheckThat(() => AssertCursorLeftIs(0)), + _.Alt_g, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "eca", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-zoo")), + CheckThat(() => AssertCursorLeftIs(8)), + _.Ctrl_z, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "ec", + TokenClassification.Prediction, "a -zoo")), + CheckThat(() => AssertCursorLeftIs(2)), + // Move cursor to column 0 again. + _.LeftArrow, _.LeftArrow, + CheckThat(() => AssertCursorLeftIs(0)), + _.Alt_f, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "eca", + TokenClassification.None, ' ', + TokenClassification.Prediction, "-zoo")), + CheckThat(() => AssertCursorLeftIs(4)) + )); + } + + [SkippableFact] + public void AcceptSuggestionWithSelection() + { + TestSetup(KeyMode.Cmd, + new KeyHandler("Ctrl+f", PSConsoleReadLine.ForwardWord)); + + SetHistory("git diff --cached", "git diff"); + Test("git diff", Keys( + "git", CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "git", + TokenClassification.Prediction, " diff")), + _.RightArrow, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "git", + TokenClassification.None, " diff")), + _.Ctrl_z, CheckThat(() => AssertCursorLeftIs(3)), + _.Ctrl_f, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "git", + TokenClassification.None, " diff", + TokenClassification.Prediction, " --cached")), + + // Perform visual selection and then accept suggestion. + _.Ctrl_z, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "git", + TokenClassification.Prediction, " diff")), + _.LeftArrow, _.Shift_RightArrow, + CheckThat(() => + { + PSConsoleReadLine.GetSelectionState(out var start, out var length); + Assert.Equal(2, start); + Assert.Equal(1, length); + }), + _.RightArrow, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "git", + TokenClassification.None, " diff")), + + // Perform visual selection and then accept next suggestion word. + _.Ctrl_z, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "git", + TokenClassification.Prediction, " diff")), + _.LeftArrow, _.Shift_RightArrow, + CheckThat(() => + { + PSConsoleReadLine.GetSelectionState(out var start, out var length); + Assert.Equal(2, start); + Assert.Equal(1, length); + }), + _.Ctrl_f, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "git", + TokenClassification.None, " diff", + TokenClassification.Prediction, " --cached")) + )); + } + [SkippableFact] public void DisablePrediction() { TestSetup(KeyMode.Cmd); SetHistory("echo -bar", "eca -zoo"); - PSConsoleReadLine.SetOptions(new SetPSReadLineOption {PredictionStyle = PredictionStyle.None}); + PSConsoleReadLine.SetOptions(new SetPSReadLineOption {PredictionSource = PredictionSource.None}); Test("ech", Keys( 'e', CheckThat(() => AssertScreenIs(1, TokenClassification.Command, 'e')), diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs index ef5e6554b..8abbef291 100644 --- a/test/UnitTestReadLine.cs +++ b/test/UnitTestReadLine.cs @@ -478,7 +478,7 @@ private void TestSetup(KeyMode keyMode, params KeyHandler[] keyHandlers) ShowToolTips = PSConsoleReadLineOptions.DefaultShowToolTips, WordDelimiters = PSConsoleReadLineOptions.DefaultWordDelimiters, PromptText = new [] {""}, - PredictionStyle = PredictionStyle.Concise, + PredictionSource = PredictionSource.History, Colors = new Hashtable { { "ContinuationPrompt", MakeCombinedColor(_console.ForegroundColor, _console.BackgroundColor) }, { "Emphasis", MakeCombinedColor(PSConsoleReadLineOptions.DefaultEmphasisColor, _console.BackgroundColor) }, From 8db6bd6b1c8bd39abe8eeaead0ed93f9663e0d8d Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 7 May 2020 16:42:22 -0700 Subject: [PATCH 4/6] Refactor 'InWord' --- PSReadLine/Words.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/PSReadLine/Words.cs b/PSReadLine/Words.cs index 4c6e014c5..40be76214 100644 --- a/PSReadLine/Words.cs +++ b/PSReadLine/Words.cs @@ -91,12 +91,11 @@ private Token FindToken(int current, FindTokenMode mode) private bool InWord(int index, string wordDelimiters) { char c = _buffer[index]; - return !char.IsWhiteSpace(c) && wordDelimiters.IndexOf(c) < 0; + return InWord(c, wordDelimiters); } - private bool InWord(string text, int index, string wordDelimiters) + private bool InWord(char c, string wordDelimiters) { - char c = text[index]; return !char.IsWhiteSpace(c) && wordDelimiters.IndexOf(c) < 0; } @@ -112,12 +111,12 @@ private int FindForwardSuggestionWordPoint(int currentIndex, string wordDelimite } int i = currentIndex; - if (!InWord(_suggestionText, i, wordDelimiters)) + if (!InWord(_suggestionText[i], wordDelimiters)) { // Scan to end of current non-word region while (++i < _suggestionText.Length) { - if (InWord(_suggestionText, i, wordDelimiters)) + if (InWord(_suggestionText[i], wordDelimiters)) { break; } @@ -128,7 +127,7 @@ private int FindForwardSuggestionWordPoint(int currentIndex, string wordDelimite { while (++i < _suggestionText.Length) { - if (!InWord(_suggestionText, i, wordDelimiters)) + if (!InWord(_suggestionText[i], wordDelimiters)) { if (_suggestionText[i] == ' ') { From 509884aff81930d9a46d925f279384b562ef0dd2 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Sat, 9 May 2020 11:34:46 -0700 Subject: [PATCH 5/6] Address new comments --- PSReadLine/Cmdlets.cs | 21 ------------ PSReadLine/History.cs | 8 +++-- PSReadLine/Movement.cs | 2 +- PSReadLine/Options.cs | 8 ++--- PSReadLine/PSReadLine.format.ps1xml | 3 -- PSReadLine/PSReadLineResources.Designer.cs | 29 ++++++++++++++++- PSReadLine/PSReadLineResources.resx | 3 ++ PSReadLine/Render.cs | 10 ------ PSReadLine/Suggestion.cs | 21 +++--------- docs/Set-PSReadLineOption.md | 24 ++------------ test/SuggestionTest.cs | 38 +++++++++++++++++++++- 11 files changed, 85 insertions(+), 82 deletions(-) diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs index 95e038efe..4e46526a3 100644 --- a/PSReadLine/Cmdlets.cs +++ b/PSReadLine/Cmdlets.cs @@ -69,11 +69,6 @@ public enum PredictionSource History, } - public enum PredictionViewStyle - { - Default, - } - public class PSConsoleReadLineOptions { public const ConsoleColor DefaultCommentColor = ConsoleColor.DarkGreen; @@ -148,8 +143,6 @@ public class PSConsoleReadLineOptions /// public const PredictionSource DefaultPredictionSource = PredictionSource.None; - public const PredictionViewStyle DefaultPredictionViewStyle = PredictionViewStyle.Default; - /// /// How long in milliseconds should we wait before concluding /// the input is not an escape sequence? @@ -178,7 +171,6 @@ public PSConsoleReadLineOptions(string hostName) HistorySaveStyle = DefaultHistorySaveStyle; AnsiEscapeTimeout = DefaultAnsiEscapeTimeout; PredictionSource = DefaultPredictionSource; - PredictionViewStyle = DefaultPredictionViewStyle; var historyFileName = hostName + "_history.txt"; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -338,11 +330,6 @@ public object ContinuationPromptColor /// public PredictionSource PredictionSource { get; set; } - /// - /// How the predictive suggestion is rendered. - /// - public PredictionViewStyle PredictionViewStyle { get; set; } - /// /// How long in milliseconds should we wait before concluding /// the input is not an escape sequence? @@ -731,14 +718,6 @@ public PredictionSource PredictionSource } internal PredictionSource? _predictionSource; - [Parameter] - public PredictionViewStyle PredictionViewStyle - { - get => _predictionViewStyle.GetValueOrDefault(); - set => _predictionViewStyle = value; - } - internal PredictionViewStyle? _predictionViewStyle; - [Parameter] public Hashtable Colors { get; set; } diff --git a/PSReadLine/History.cs b/PSReadLine/History.cs index 1fc4d6f91..308ec549b 100644 --- a/PSReadLine/History.cs +++ b/PSReadLine/History.cs @@ -690,9 +690,13 @@ private string GetHistorySuggestion(string text) for (int index = _history.Count - 1; index >= 0; index --) { var line = _history[index].CommandLine.TrimEnd(); - if (line.Length > text.Length && !LineIsMultiLine(line) && line.StartsWith(text, Options.HistoryStringComparison)) + if (line.Length > text.Length) { - return line; + bool isMultiLine = line.Contains('\n'); + if (!isMultiLine && line.StartsWith(text, Options.HistoryStringComparison)) + { + return line; + } } } diff --git a/PSReadLine/Movement.cs b/PSReadLine/Movement.cs index 9d626d796..d7369c2b1 100644 --- a/PSReadLine/Movement.cs +++ b/PSReadLine/Movement.cs @@ -63,7 +63,7 @@ public static void ForwardChar(ConsoleKeyInfo? key = null, object arg = null) { if (_singleton._current == _singleton._buffer.Length && numericArg > 0) { - AcceptSuggestion(); + AcceptSuggestion(key, arg); } else { diff --git a/PSReadLine/Options.cs b/PSReadLine/Options.cs index dabc5bfbe..da878cbd6 100644 --- a/PSReadLine/Options.cs +++ b/PSReadLine/Options.cs @@ -137,12 +137,12 @@ private void SetOptionsInternal(SetPSReadLineOption options) } if (options._predictionSource.HasValue) { + if (_console is PlatformWindows.LegacyWin32Console && options.PredictionSource != PredictionSource.None) + { + throw new ArgumentException(PSReadLineResources.PredictiveSuggestionNotSupported); + } Options.PredictionSource = options.PredictionSource; } - if (options._predictionViewStyle.HasValue) - { - Options.PredictionViewStyle = options.PredictionViewStyle; - } if (options.Colors != null) { IDictionaryEnumerator e = options.Colors.GetEnumerator(); diff --git a/PSReadLine/PSReadLine.format.ps1xml b/PSReadLine/PSReadLine.format.ps1xml index 306d09269..9b239328e 100644 --- a/PSReadLine/PSReadLine.format.ps1xml +++ b/PSReadLine/PSReadLine.format.ps1xml @@ -161,9 +161,6 @@ $d = [Microsoft.PowerShell.KeyHandler]::GetGroupingDescription($_.Group) PredictionSource - - PredictionViewStyle - [Microsoft.PowerShell.VTColorUtils]::FormatColor($_.CommandColor) diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index 3ec3e6b6e..03943aeef 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -638,7 +638,34 @@ internal static string InsertLineBelowDescription { return ResourceManager.GetString("InsertLineBelowDescription", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Accept the current inline or selected suggestion. + /// + internal static string AcceptSuggestionDescription { + get { + return ResourceManager.GetString("AcceptSuggestionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Accept the next word of the inline or selected suggestion. + /// + internal static string AcceptNextSuggestionWordDescription { + get { + return ResourceManager.GetString("AcceptNextSuggestionWordDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The predictive suggestion feature cannot be enabled because the console output doesn't support virtual terminal processing or it's redirected. + /// + internal static string PredictiveSuggestionNotSupported { + get { + return ResourceManager.GetString("PredictiveSuggestionNotSupported", resourceCulture); + } + } + /// /// Looks up a localized string similar to '{0}' is not a valid color property. /// diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index 0f5ae7810..e6f2d7192 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -789,4 +789,7 @@ Or not saving history with: '{0}' is not a valid color property + + The predictive suggestion feature cannot be enabled because the console output doesn't support virtual terminal processing or it's redirected. + diff --git a/PSReadLine/Render.cs b/PSReadLine/Render.cs index 83ad17b87..1eea51cc9 100644 --- a/PSReadLine/Render.cs +++ b/PSReadLine/Render.cs @@ -1200,16 +1200,6 @@ private bool LineIsMultiLine() return false; } - private bool LineIsMultiLine(string text) - { - for (int i = 0; i < text.Length; i++) - { - if (text[i] == '\n') - return true; - } - return false; - } - private int GetStatusLineCount() { if (_statusLinePrompt == null) diff --git a/PSReadLine/Suggestion.cs b/PSReadLine/Suggestion.cs index 6ae513979..107ecb07e 100644 --- a/PSReadLine/Suggestion.cs +++ b/PSReadLine/Suggestion.cs @@ -5,7 +5,7 @@ namespace Microsoft.PowerShell public partial class PSConsoleReadLine { private string _suggestionText; - private string _userInputText; + private string _lastUserInput; private bool _showSuggestion = true; private bool IsPredictionOn => _options.PredictionSource != PredictionSource.None && _showSuggestion; @@ -27,7 +27,7 @@ private IDisposable PredictionOff() private void ResetSuggestion() { _suggestionText = null; - _userInputText = null; + _lastUserInput = null; } private string GetSuggestion(string text) @@ -41,9 +41,9 @@ private string GetSuggestion(string text) try { if (_suggestionText == null || _suggestionText.Length <= text.Length || - text.Length < _userInputText.Length || !_suggestionText.StartsWith(text, _options.HistoryStringComparison)) + text.Length < _lastUserInput.Length || !_suggestionText.StartsWith(text, _options.HistoryStringComparison)) { - _userInputText = text; + _lastUserInput = text; _suggestionText = GetHistorySuggestion(text); } } @@ -59,19 +59,6 @@ private string GetSuggestion(string text) /// Accept the suggestion text if there is one. /// public static void AcceptSuggestion(ConsoleKeyInfo? key = null, object arg = null) - { - if (!TryGetArgAsInt(arg, out var numericArg, 1)) - { - return; - } - - if (numericArg > 0) - { - AcceptSuggestion(); - } - } - - private static void AcceptSuggestion() { if (_singleton._suggestionText != null) { diff --git a/docs/Set-PSReadLineOption.md b/docs/Set-PSReadLineOption.md index 883ee5726..db0ea0aeb 100644 --- a/docs/Set-PSReadLineOption.md +++ b/docs/Set-PSReadLineOption.md @@ -36,7 +36,7 @@ Set-PSReadLineOption [-AnsiEscapeTimeout ] [-ViModeIndicator ] [-ViModeChangeHandler ] - [-PredictionStyle ] + [-PredictionSource ] ``` ## DESCRIPTION @@ -626,7 +626,7 @@ Valid values are: -- History: get predictive suggestions from history only. ```yaml -Type: PredictionStyle +Type: PredictionSource Parameter Sets: (All) Aliases: @@ -637,26 +637,6 @@ Accept pipeline input: false Accept wildcard characters: false ``` -### -PredictionViewStyle - -Specifies how PSReadLine should render the predictive suggestions. - -Valid values are: - --- Default: Render the suggestion in an inline manner. - -```yaml -Type: PredictionStyle -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: Default -Accept pipeline input: false -Accept wildcard characters: false -``` - ## INPUTS ### None diff --git a/test/SuggestionTest.cs b/test/SuggestionTest.cs index 34fdd87d3..c62736b5a 100644 --- a/test/SuggestionTest.cs +++ b/test/SuggestionTest.cs @@ -19,7 +19,8 @@ public void RenderSuggestion() 'a', CheckThat(() => AssertScreenIs(1, TokenClassification.Command, 'a')), _.Ctrl_f, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, 'a')), _.RightArrow, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, 'a')), - CheckThat(() => AssertCursorLeftIs(1)))); + CheckThat(() => AssertCursorLeftIs(1)) + )); // Different matches as more input coming SetHistory("echo -bar", "eca -zoo"); @@ -130,6 +131,41 @@ public void CustomKeyBindingsToAcceptSuggestion() )); } + [SkippableFact] + public void AcceptNextSuggestionWordCanAcceptMoreThanOneWords() + { + TestSetup(KeyMode.Cmd, + new KeyHandler("Ctrl+f", PSConsoleReadLine.ForwardWord), + new KeyHandler("Alt+f", PSConsoleReadLine.AcceptNextSuggestionWord)); + + SetHistory("abc def ghi jkl"); + Test("abc def ghi jkl", Keys( + 'a', CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, 'a', + TokenClassification.Prediction, "bc def ghi jkl")), + _.Alt_3, _.Ctrl_f, + CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "abc", + TokenClassification.None, " def ghi ", + TokenClassification.Prediction, "jkl")), + _.Ctrl_z, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, 'a', + TokenClassification.Prediction, "bc def ghi jkl")), + _.Alt_3, _.Alt_f, + CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "abc", + TokenClassification.None, " def ghi ", + TokenClassification.Prediction, "jkl")), + _.Ctrl_z, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, 'a', + TokenClassification.Prediction, "bc def ghi jkl")), + _.Alt_8, _.Alt_f, + CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "abc", + TokenClassification.None, " def ghi jkl")) + )); + } + [SkippableFact] public void AcceptSuggestionWithSelection() { From 57e054acfc9f55beeb446cf4b4da964e3e9a39d8 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Sat, 9 May 2020 11:50:11 -0700 Subject: [PATCH 6/6] Revert the refactoring to 'SelfInsert' --- PSReadLine/BasicEditing.cs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/PSReadLine/BasicEditing.cs b/PSReadLine/BasicEditing.cs index 70c95dd57..c6b0823be 100644 --- a/PSReadLine/BasicEditing.cs +++ b/PSReadLine/BasicEditing.cs @@ -31,20 +31,26 @@ public static void SelfInsert(ConsoleKeyInfo? key = null, object arg = null) { if (count <= 0) return; - } - else - { - count = 1; + if (count > 1) + { + var toInsert = new string(keyChar, count); + if (_singleton._visualSelectionCommandCount > 0) + { + _singleton.GetRegion(out var start, out var length); + Replace(start, length, toInsert); + } + else + { + Insert(toInsert); + } + return; + } } if (_singleton._visualSelectionCommandCount > 0) { _singleton.GetRegion(out var start, out var length); - Replace(start, length, new string(keyChar, count)); - } - else if (count > 1) - { - Insert(new string(keyChar, count)); + Replace(start, length, new string(keyChar, 1)); } else {