/********************************************************************++
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Text;
using Microsoft.PowerShell.Internal;
namespace Microsoft.PowerShell
{
public partial class PSConsoleReadLine
{
// Tab completion state
private int _tabCommandCount;
private CommandCompletion _tabCompletions;
private Runspace _runspace;
// String helper for directory paths
private static string DirectorySeparatorString = System.IO.Path.DirectorySeparatorChar.ToString();
// Stub helper method so completion can be mocked
[ExcludeFromCodeCoverage]
CommandCompletion IPSConsoleReadLineMockableMethods.CompleteInput(string input, int cursorIndex, Hashtable options, System.Management.Automation.PowerShell powershell)
{
return CalloutUsingDefaultConsoleMode(
() => CommandCompletion.CompleteInput(input, cursorIndex, options, powershell));
}
///
/// Attempt to complete the text surrounding the cursor with the next
/// available completion.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void TabCompleteNext(ConsoleKeyInfo? key = null, object arg = null)
{
_singleton.Complete(forward: true);
}
///
/// Attempt to complete the text surrounding the cursor with the previous
/// available completion.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void TabCompletePrevious(ConsoleKeyInfo? key = null, object arg = null)
{
_singleton.Complete(forward: false);
}
private static bool IsSingleQuote(char c)
{
return c == '\'' || c == (char)8216 || c == (char)8217 || c == (char)8218;
}
private static bool IsDoubleQuote(char c)
{
return c == '"' || c == (char)8220 || c == (char)8221;
}
private static bool IsQuoted(string s)
{
if (s.Length >= 2)
{
var first = s[0];
var last = s[s.Length - 1];
return ((IsSingleQuote(first) && IsSingleQuote(last))
||
(IsDoubleQuote(first) && IsDoubleQuote(last)));
}
return false;
}
private static string GetUnquotedText(string s, bool consistentQuoting)
{
if (!consistentQuoting && IsQuoted(s))
{
s = s.Substring(1, s.Length - 2);
}
return s;
}
///
/// Attempt to perform completion on the text surrounding the cursor.
/// If there are multiple possible completions, the longest unambiguous
/// prefix is used for completion. If trying to complete the longest
/// unambiguous completion, a list of possible completions is displayed.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void Complete(ConsoleKeyInfo? key = null, object arg = null)
{
_singleton.CompleteImpl(key, arg, false);
}
///
/// Attempt to perform completion on the text surrounding the cursor.
/// If there are multiple possible completions, the longest unambiguous
/// prefix is used for completion. If trying to complete the longest
/// unambiguous completion, a list of possible completions is displayed.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void MenuComplete(ConsoleKeyInfo? key = null, object arg = null)
{
_singleton.CompleteImpl(key, arg, true);
}
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
private void CompleteImpl(ConsoleKeyInfo? key, object arg, bool menuSelect)
{
var completions = GetCompletions();
if (completions == null || completions.CompletionMatches.Count == 0)
return;
if (_tabCommandCount > 0)
{
if (completions.CompletionMatches.Count == 1)
{
Ding();
}
else
{
PossibleCompletionsImpl(completions, menuSelect);
}
return;
}
if (completions.CompletionMatches.Count == 1)
{
// We want to add a backslash for directory completion if possible. This
// is mostly only needed if we have a single completion - if there are multiple
// completions, then we'll be showing the possible completions where it's very
// unlikely that we would add a trailing backslash.
DoReplacementForCompletion(completions.CompletionMatches[0], completions);
return;
}
if (menuSelect)
{
PossibleCompletionsImpl(completions, true);
return;
}
// Find the longest unambiguous prefix. This might be the empty
// string, in which case we don't want to remove any of the users input,
// instead we'll immediately show possible completions.
// For the purposes of unambiguous prefix, we'll ignore quotes if
// some completions aren't quoted.
var firstResult = completions.CompletionMatches[0];
int quotedCompletions = completions.CompletionMatches.Count(match => IsQuoted(match.CompletionText));
bool consistentQuoting =
quotedCompletions == 0 ||
(quotedCompletions == completions.CompletionMatches.Count &&
quotedCompletions == completions.CompletionMatches.Count(
m => m.CompletionText[0] == firstResult.CompletionText[0]));
bool ambiguous = false;
var replacementText = GetUnquotedText(firstResult.CompletionText, consistentQuoting);
foreach (var match in completions.CompletionMatches.Skip(1))
{
var matchText = GetUnquotedText(match.CompletionText, consistentQuoting);
for (int i = 0; i < replacementText.Length; i++)
{
if (i == matchText.Length
|| char.ToLowerInvariant(replacementText[i]) != char.ToLowerInvariant(matchText[i]))
{
ambiguous = true;
replacementText = replacementText.Substring(0, i);
break;
}
}
if (replacementText.Length == 0)
{
break;
}
}
if (replacementText.Length > 0)
{
Replace(completions.ReplacementIndex, completions.ReplacementLength, replacementText);
completions.ReplacementLength = replacementText.Length;
if (ambiguous)
{
Ding();
}
}
else
{
// No common prefix, don't wait for a second tab, just show the possible completions
// right away.
PossibleCompletionsImpl(completions, false);
}
_tabCommandCount += 1;
}
private CommandCompletion GetCompletions()
{
if (_tabCommandCount == 0)
{
try
{
_tabCompletions = null;
// Could use the overload that takes an AST as it's faster (we've already parsed the
// input for coloring) but that overload is a little more complicated in passing in the
// cursor position.
System.Management.Automation.PowerShell ps;
if (!_mockableMethods.RunspaceIsRemote(_runspace))
{
ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);
}
else
{
ps = System.Management.Automation.PowerShell.Create();
ps.Runspace = _runspace;
}
_tabCompletions = _mockableMethods.CompleteInput(_buffer.ToString(), _current, null, ps);
if (_tabCompletions.CompletionMatches.Count == 0)
return null;
}
catch (Exception)
{
}
}
return _tabCompletions;
}
private void Complete(bool forward)
{
var completions = GetCompletions();
if (completions == null)
return;
completions.CurrentMatchIndex += forward ? 1 : -1;
if (completions.CurrentMatchIndex < 0)
{
completions.CurrentMatchIndex = completions.CompletionMatches.Count - 1;
}
else if (completions.CurrentMatchIndex == completions.CompletionMatches.Count)
{
completions.CurrentMatchIndex = 0;
}
var completionResult = completions.CompletionMatches[completions.CurrentMatchIndex];
DoReplacementForCompletion(completionResult, completions);
_tabCommandCount += 1;
}
private void DoReplacementForCompletion(CompletionResult completionResult, CommandCompletion completions)
{
var replacementText = completionResult.CompletionText;
int cursorAdjustment = 0;
if (completionResult.ResultType == CompletionResultType.ProviderContainer)
{
replacementText = GetReplacementTextForDirectory(replacementText, ref cursorAdjustment);
}
Replace(completions.ReplacementIndex, completions.ReplacementLength, replacementText);
if (cursorAdjustment != 0)
{
_current += cursorAdjustment;
PlaceCursor();
}
completions.ReplacementLength = replacementText.Length;
}
private static string GetReplacementTextForDirectory(string replacementText, ref int cursorAdjustment)
{
if (!replacementText.EndsWith(DirectorySeparatorString , StringComparison.Ordinal))
{
if (replacementText.EndsWith(String.Format("{0}\'", DirectorySeparatorString), StringComparison.Ordinal) ||
replacementText.EndsWith(String.Format("{0}\"", DirectorySeparatorString), StringComparison.Ordinal))
{
cursorAdjustment = -1;
}
else if (replacementText.EndsWith("'", StringComparison.Ordinal) ||
replacementText.EndsWith("\"", StringComparison.Ordinal))
{
var len = replacementText.Length;
replacementText = replacementText.Substring(0, len - 1) + System.IO.Path.DirectorySeparatorChar + replacementText[len - 1];
cursorAdjustment = -1;
}
else
{
replacementText = replacementText + System.IO.Path.DirectorySeparatorChar;
}
}
return replacementText;
}
private static void InvertSelectedCompletion(BufferChar[] buffer, int selectedItem, int menuColumnWidth, int menuRows)
{
var selectedX = selectedItem / menuRows;
var selectedY = selectedItem - (selectedX * menuRows);
var start = selectedY * _singleton._console.BufferWidth + selectedX * menuColumnWidth;
for (int i = 0; i < menuColumnWidth; i++)
{
buffer[i + start].Inverse = true;
}
}
///
/// Display the list of possible completions.
///
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
public static void PossibleCompletions(ConsoleKeyInfo? key = null, object arg = null)
{
var completions = _singleton.GetCompletions();
_singleton.PossibleCompletionsImpl(completions, menuSelect: false);
}
private static string HandleNewlinesForPossibleCompletions(string s)
{
s = s.Trim();
var newlineIndex = s.IndexOfAny(new []{'\r', '\n'});
if (newlineIndex >= 0)
{
s = s.Substring(0, newlineIndex) + "...";
}
return s;
}
private void PossibleCompletionsImpl(CommandCompletion completions, bool menuSelect)
{
if (completions == null || completions.CompletionMatches.Count == 0)
{
Ding();
return;
}
if (completions.CompletionMatches.Count >= _options.CompletionQueryItems)
{
if (!PromptYesOrNo(string.Format(CultureInfo.CurrentCulture, PSReadLineResources.DisplayAllPossibilities, completions.CompletionMatches.Count)))
{
return;
}
}
var matches = completions.CompletionMatches;
var minColWidth = matches.Max(c => c.ListItemText.Length);
minColWidth += 2;
var menuColumnWidth = minColWidth;
int displayRows;
var bufferWidth = _console.BufferWidth;
ConsoleBufferBuilder cb;
if (Options.ShowToolTips)
{
const string seperator = "- ";
var maxTooltipWidth = bufferWidth - minColWidth - seperator.Length;
displayRows = matches.Count;
cb = new ConsoleBufferBuilder(displayRows * bufferWidth, _console);
for (int index = 0; index < matches.Count; index++)
{
var match = matches[index];
var listItemText = HandleNewlinesForPossibleCompletions(match.ListItemText);
cb.Append(listItemText);
var spacesNeeded = minColWidth - listItemText.Length;
if (spacesNeeded > 0)
{
cb.Append(' ', spacesNeeded);
}
cb.Append(seperator);
var toolTip = HandleNewlinesForPossibleCompletions(match.ToolTip);
toolTip = toolTip.Length <= maxTooltipWidth
? toolTip
: toolTip.Substring(0, maxTooltipWidth);
cb.Append(toolTip);
// Make sure we always write out exactly 1 buffer width
spacesNeeded = (bufferWidth * (index + 1)) - cb.Length;
if (spacesNeeded > 0)
{
cb.Append(' ', spacesNeeded);
}
}
menuColumnWidth = bufferWidth;
}
else
{
var screenColumns = bufferWidth;
var displayColumns = Math.Max(1, screenColumns / minColWidth);
displayRows = (completions.CompletionMatches.Count + displayColumns - 1) / displayColumns;
cb = new ConsoleBufferBuilder(displayRows * bufferWidth, _console);
for (var row = 0; row < displayRows; row++)
{
for (var col = 0; col < displayColumns; col++)
{
var index = row + (displayRows * col);
if (index >= matches.Count)
break;
var match = matches[index];
var item = HandleNewlinesForPossibleCompletions(match.ListItemText);
cb.Append(item);
cb.Append(' ', minColWidth - item.Length);
}
// Make sure we always write out exactly 1 buffer width
var spacesNeeded = (bufferWidth * (row + 1)) - cb.Length;
if (spacesNeeded > 0)
{
cb.Append(' ', spacesNeeded);
}
}
}
var menuBuffer = cb.ToArray();
if (menuSelect)
{
// Make sure the menu and line can appear on the screen at the same time,
// if not, we'll skip the menu.
var endBufferCoords = ConvertOffsetToCoordinates(_buffer.Length);
var bufferLines = endBufferCoords.Y - _initialY + 1;
if ((bufferLines + displayRows) > _console.WindowHeight)
{
menuSelect = false;
}
}
if (menuSelect)
{
RemoveEditsAfterUndo();
var undoPoint = _edits.Count;
int selectedItem = 0;
bool undo = false;
DoReplacementForCompletion(matches[0], completions);
// Recompute end of buffer coordinates as the replacement could have
// added a line.
var endBufferCoords = ConvertOffsetToCoordinates(_buffer.Length);
var menuAreaTop = endBufferCoords.Y + 1;
var previousMenuTop = menuAreaTop;
InvertSelectedCompletion(menuBuffer, selectedItem, menuColumnWidth, displayRows);
_console.WriteBufferLines(menuBuffer, ref menuAreaTop);
// Showing the menu may have scrolled the screen or moved the cursor, update initialY to reflect that.
_initialY -= (previousMenuTop - menuAreaTop);
PlaceCursor();
previousMenuTop = menuAreaTop;
int previousItem = selectedItem;
bool processingKeys = true;
while (processingKeys)
{
var nextKey = ReadKey();
if (nextKey == Keys.RightArrow)
{
selectedItem = Math.Min(selectedItem + displayRows, matches.Count - 1);
}
else if (nextKey == Keys.LeftArrow)
{
selectedItem = Math.Max(selectedItem - displayRows, 0);
}
else if (nextKey == Keys.DownArrow)
{
selectedItem = Math.Min(selectedItem + 1, matches.Count - 1);
}
else if (nextKey == Keys.UpArrow)
{
selectedItem = Math.Max(selectedItem - 1, 0);
}
else if (nextKey == Keys.Tab)
{
selectedItem = (selectedItem + 1) % matches.Count;
}
else if (nextKey == Keys.ShiftTab)
{
selectedItem = (selectedItem - 1) % matches.Count;
if (selectedItem < 0)
{
selectedItem += matches.Count;
}
}
else if (nextKey == Keys.CtrlG || nextKey == Keys.Escape)
{
undo = true;
processingKeys = false;
}
else
{
PrependQueuedKeys(nextKey);
processingKeys = false;
}
if (selectedItem != previousItem)
{
DoReplacementForCompletion(matches[selectedItem], completions);
endBufferCoords = ConvertOffsetToCoordinates(_buffer.Length);
menuAreaTop = endBufferCoords.Y + 1;
InvertSelectedCompletion(menuBuffer, previousItem, menuColumnWidth, displayRows);
InvertSelectedCompletion(menuBuffer, selectedItem, menuColumnWidth, displayRows);
_console.WriteBufferLines(menuBuffer, ref menuAreaTop);
previousItem = selectedItem;
if (previousMenuTop > menuAreaTop)
{
WriteBlankLines(previousMenuTop - menuAreaTop, menuAreaTop + displayRows);
}
}
}
WriteBlankLines(displayRows, menuAreaTop);
var lastInsert = ((GroupedEdit)_edits[_edits.Count - 1])._groupedEditItems[1];
Debug.Assert(lastInsert is EditItemInsertString, "The only edits possible here are pairs of Delete/Insert");
var firstDelete = ((GroupedEdit)_edits[undoPoint])._groupedEditItems[0];
Debug.Assert(firstDelete is EditItemDelete, "The only edits possible here are pairs of Delete/Insert");
var groupEditCount = _edits.Count - undoPoint;
_edits.RemoveRange(undoPoint, groupEditCount);
_undoEditIndex = undoPoint;
if (undo)
{
// Pretend it never happened.
lastInsert.Undo();
firstDelete.Undo();
Render();
}
else
{
// Leave one edit instead of possibly many to undo
SaveEditItem(GroupedEdit.Create(new List { firstDelete, lastInsert }));
}
}
else
{
var endBufferCoords = ConvertOffsetToCoordinates(_buffer.Length);
var menuAreaTop = endBufferCoords.Y + 1;
_console.WriteBufferLines(menuBuffer, ref menuAreaTop);
_initialY = menuAreaTop + displayRows;
Render();
}
}
}
}