diff --git a/PSReadLine/BasicEditing.cs b/PSReadLine/BasicEditing.cs
index bbfbd39ec..c6b0823be 100644
--- a/PSReadLine/BasicEditing.cs
+++ b/PSReadLine/BasicEditing.cs
@@ -85,6 +85,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 +216,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 +278,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)
+ {
+ ResetSuggestion();
+ _console.BlankRestOfLine();
+ }
+
_console.Write("\n");
_inputAccepted = true;
return true;
diff --git a/PSReadLine/Changes.txt b/PSReadLine/Changes.txt
index dbef3b6b6..31c5c2f48 100644
--- a/PSReadLine/Changes.txt
+++ b/PSReadLine/Changes.txt
@@ -1,3 +1,9 @@
+### Version 2.1.0-beta1
+
+Pre-release notes:
+
+* Experimental support for fish-like suggestions in PSReadLine.
+
### Version 2.0.1
Bug fixes:
diff --git a/PSReadLine/Cmdlets.cs b/PSReadLine/Cmdlets.cs
index c3d569b65..4e46526a3 100644
--- a/PSReadLine/Cmdlets.cs
+++ b/PSReadLine/Cmdlets.cs
@@ -63,6 +63,12 @@ public enum AddToHistoryOption
MemoryAndFile
}
+ public enum PredictionSource
+ {
+ None,
+ History,
+ }
+
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,11 @@ public class PSConsoleReadLineOptions
public const HistorySaveStyle DefaultHistorySaveStyle = HistorySaveStyle.SaveIncrementally;
+ ///
+ /// The predictive suggestion feature is disabled by default.
+ ///
+ public const PredictionSource DefaultPredictionSource = PredictionSource.None;
+
///
/// How long in milliseconds should we wait before concluding
/// the input is not an escape sequence?
@@ -155,6 +170,7 @@ public PSConsoleReadLineOptions(string hostName)
HistorySearchCaseSensitive = DefaultHistorySearchCaseSensitive;
HistorySaveStyle = DefaultHistorySaveStyle;
AnsiEscapeTimeout = DefaultAnsiEscapeTimeout;
+ PredictionSource = DefaultPredictionSource;
var historyFileName = hostName + "_history.txt";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@@ -309,6 +325,11 @@ 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 long in milliseconds should we wait before concluding
/// the input is not an escape sequence?
@@ -407,6 +428,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 +448,7 @@ public object SelectionColor
internal string _emphasisColor;
internal string _errorColor;
internal string _selectionColor;
+ internal string _predictionColor;
internal void ResetColors()
{
@@ -438,6 +466,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 +502,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 +710,14 @@ public ViModeStyle ViModeIndicator
[Parameter]
public ScriptBlock ViModeChangeHandler { get; set; }
+ [Parameter]
+ public PredictionSource PredictionSource
+ {
+ get => _predictionSource.GetValueOrDefault();
+ set => _predictionSource = value;
+ }
+ internal PredictionSource? _predictionSource;
+
[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..308ec549b 100644
--- a/PSReadLine/History.cs
+++ b/PSReadLine/History.cs
@@ -502,6 +502,8 @@ private void UpdateFromHistory(HistoryMoveCursor moveCursor)
}
break;
}
+
+ using var _ = PredictionOff();
Render();
}
@@ -677,6 +679,30 @@ 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)
+ {
+ bool isMultiLine = line.Contains('\n');
+ if (!isMultiLine && line.StartsWith(text, Options.HistoryStringComparison))
+ {
+ return line;
+ }
+ }
+ }
+
+ return null;
+ }
+
///
/// Move to the first item in the history.
///
@@ -889,6 +915,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/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/Movement.cs b/PSReadLine/Movement.cs
index 138191969..d7369c2b1 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(key, arg);
+ }
+ 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..da878cbd6 100644
--- a/PSReadLine/Options.cs
+++ b/PSReadLine/Options.cs
@@ -135,6 +135,14 @@ private void SetOptionsInternal(SetPSReadLineOption options)
{
Options.PromptText = options.PromptText;
}
+ if (options._predictionSource.HasValue)
+ {
+ if (_console is PlatformWindows.LegacyWin32Console && options.PredictionSource != PredictionSource.None)
+ {
+ throw new ArgumentException(PSReadLineResources.PredictiveSuggestionNotSupported);
+ }
+ Options.PredictionSource = options.PredictionSource;
+ }
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..9b239328e 100644
--- a/PSReadLine/PSReadLine.format.ps1xml
+++ b/PSReadLine/PSReadLine.format.ps1xml
@@ -158,6 +158,9 @@ $d = [Microsoft.PowerShell.KeyHandler]::GetGroupingDescription($_.Group)
AnsiEscapeTimeout
+
+ PredictionSource
+
[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/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 fd9def428..e6f2d7192 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}
@@ -783,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/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..1eea51cc9 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;
@@ -172,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')
{
- currColor = _options._emphasisColor;
+ 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))
+ {
+ _consoleBufferLines[currentLogicalLine].Append('^');
+ _consoleBufferLines[currentLogicalLine].Append((char)('@' + charToRender));
+ }
+ else
+ {
+ _consoleBufferLines[currentLogicalLine].Append(charToRender);
}
- UpdateColorsIfNecessary(currColor);
}
foreach (var buf in _consoleBufferLines)
@@ -288,58 +328,24 @@ 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 - 1)
- {
- _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);
- foreach (char c in Options.ContinuationPrompt)
- {
- _consoleBufferLines[currentLogicalLine].Append(c);
- }
+ var toEmphasize = i >= _emphasisStart && i < (_emphasisStart + _emphasisLength);
- 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;
+ }
+
+ if (suggestion != null)
+ {
+ color = _options._predictionColor;
+ foreach (char charToRender in suggestion)
+ {
+ RenderOneChar(charToRender, toEmphasize: false);
+ }
}
if (_statusLinePrompt != null)
diff --git a/PSReadLine/Suggestion.cs b/PSReadLine/Suggestion.cs
new file mode 100644
index 000000000..107ecb07e
--- /dev/null
+++ b/PSReadLine/Suggestion.cs
@@ -0,0 +1,106 @@
+using System;
+
+namespace Microsoft.PowerShell
+{
+ public partial class PSConsoleReadLine
+ {
+ private string _suggestionText;
+ private string _lastUserInput;
+ 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 IDisposable PredictionOff()
+ {
+ var oldSuggestionMode = _singleton._showSuggestion;
+ _singleton._showSuggestion = false;
+
+ return new Disposable(() => _singleton._showSuggestion = oldSuggestionMode);
+ }
+
+ private void ResetSuggestion()
+ {
+ _suggestionText = null;
+ _lastUserInput = null;
+ }
+
+ private string GetSuggestion(string text)
+ {
+ if (IsPredictionOff || LineIsMultiLine() || string.IsNullOrWhiteSpace(text))
+ {
+ ResetSuggestion();
+ return null;
+ }
+
+ try
+ {
+ if (_suggestionText == null || _suggestionText.Length <= text.Length ||
+ text.Length < _lastUserInput.Length || !_suggestionText.StartsWith(text, _options.HistoryStringComparison))
+ {
+ _lastUserInput = text;
+ _suggestionText = GetHistorySuggestion(text);
+ }
+ }
+ catch
+ {
+ _suggestionText = null;
+ }
+
+ return _suggestionText?.Substring(text.Length);
+ }
+
+ ///
+ /// Accept the suggestion text if there is one.
+ ///
+ public static void AcceptSuggestion(ConsoleKeyInfo? key = null, object arg = null)
+ {
+ 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 a9ea20289..40be76214 100644
--- a/PSReadLine/Words.cs
+++ b/PSReadLine/Words.cs
@@ -91,9 +91,59 @@ private Token FindToken(int current, FindTokenMode mode)
private bool InWord(int index, string wordDelimiters)
{
char c = _buffer[index];
+ return InWord(c, wordDelimiters);
+ }
+
+ private bool InWord(char c, string wordDelimiters)
+ {
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 sure 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..db0ea0aeb 100644
--- a/docs/Set-PSReadLineOption.md
+++ b/docs/Set-PSReadLineOption.md
@@ -36,6 +36,7 @@ Set-PSReadLineOption
[-AnsiEscapeTimeout ]
[-ViModeIndicator ]
[-ViModeChangeHandler ]
+ [-PredictionSource ]
```
## 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
```
+### -PredictionSource
+
+Specifies the source for PSReadLine to get predictive suggestions.
+
+Valid values are:
+
+-- None: disable the predictive suggestion feature.
+
+-- History: get predictive suggestions from history only.
+
+```yaml
+Type: PredictionSource
+Parameter Sets: (All)
+Aliases:
+
+Required: False
+Position: Named
+Default value: None
+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..c62736b5a
--- /dev/null
+++ b/test/SuggestionTest.cs
@@ -0,0 +1,293 @@
+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 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 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()
+ {
+ 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 {PredictionSource = PredictionSource.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"))
+ ));
+ }
+
+ [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));
+ }
+ }
+}
diff --git a/test/UnitTestReadLine.cs b/test/UnitTestReadLine.cs
index b17e3345a..8abbef291 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 [] {""},
+ PredictionSource = PredictionSource.History,
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++)