mirror of
https://github.com/aphrody-labs/PowerShell.git
synced 2026-10-17 20:56:02 +00:00
CHAR_INFO has 4 bits for ConsoleColor. On Linux, ConsoleColor is sometimes -1 (unknown), which is a 17th possible value for ConsoleColor, which can't be represented in CHAR_INFO. To fix this, I've introduced a bigger type that gets converted to CHAR_INFO when necessary.
557 lines
22 KiB
C#
557 lines
22 KiB
C#
/********************************************************************++
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to complete the text surrounding the cursor with the next
|
|
/// available completion.
|
|
/// </summary>
|
|
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
|
|
public static void TabCompleteNext(ConsoleKeyInfo? key = null, object arg = null)
|
|
{
|
|
_singleton.Complete(forward: true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempt to complete the text surrounding the cursor with the previous
|
|
/// available completion.
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
|
|
public static void Complete(ConsoleKeyInfo? key = null, object arg = null)
|
|
{
|
|
_singleton.CompleteImpl(key, arg, false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Display the list of possible completions.
|
|
/// </summary>
|
|
[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<EditItem> { firstDelete, lastInsert }));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var endBufferCoords = ConvertOffsetToCoordinates(_buffer.Length);
|
|
var menuAreaTop = endBufferCoords.Y + 1;
|
|
|
|
_console.WriteBufferLines(menuBuffer, ref menuAreaTop);
|
|
_initialY = menuAreaTop + displayRows;
|
|
Render();
|
|
}
|
|
}
|
|
}
|
|
}
|