Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the 'SelectCommandArgument' bind-able function #2222

Merged
merged 1 commit into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 34 additions & 32 deletions PSReadLine/DynamicHelp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,11 @@ namespace Microsoft.PowerShell
{
public partial class PSConsoleReadLine
{
private Microsoft.PowerShell.Pager _pager;

/// <summary>
/// Attempt to show help content.
/// Show the full help for the command on the alternate screen buffer.
/// </summary>
public static void ShowCommandHelp(ConsoleKeyInfo? key = null, object arg = null)
{
if (_singleton._console is PlatformWindows.LegacyWin32Console)
{
Collection<string> helpBlock = new Collection<string>()
{
string.Empty,
PSReadLineResources.FullHelpNotSupportedInLegacyConsole
};

_singleton.WriteDynamicHelpBlock(helpBlock);

return;
}

_singleton.DynamicHelpImpl(isFullHelp: true);
}

/// <summary>
/// Attempt to show help content.
/// Show the short help of the parameter next to the cursor.
/// </summary>
public static void ShowParameterHelp(ConsoleKeyInfo? key = null, object arg = null)
// Stub helper methods so dynamic help can be mocked
[ExcludeFromCodeCoverage]
void IPSConsoleReadLineMockableMethods.RenderFullHelp(string content, string regexPatternToScrollTo)
{
_singleton.DynamicHelpImpl(isFullHelp: false);
_pager.Write(content, regexPatternToScrollTo);
}

[ExcludeFromCodeCoverage]
Expand Down Expand Up @@ -112,9 +86,37 @@ object IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandNam
}
}

void IPSConsoleReadLineMockableMethods.RenderFullHelp(string content, string regexPatternToScrollTo)
private Pager _pager;

/// <summary>
/// Attempt to show help content.
/// Show the full help for the command on the alternate screen buffer.
/// </summary>
public static void ShowCommandHelp(ConsoleKeyInfo? key = null, object arg = null)
{
_pager.Write(content, regexPatternToScrollTo);
if (_singleton._console is PlatformWindows.LegacyWin32Console)
{
Collection<string> helpBlock = new Collection<string>()
{
string.Empty,
PSReadLineResources.FullHelpNotSupportedInLegacyConsole
};

_singleton.WriteDynamicHelpBlock(helpBlock);

return;
}

_singleton.DynamicHelpImpl(isFullHelp: true);
}

/// <summary>
/// Attempt to show help content.
/// Show the short help of the parameter next to the cursor.
/// </summary>
public static void ShowParameterHelp(ConsoleKeyInfo? key = null, object arg = null)
{
_singleton.DynamicHelpImpl(isFullHelp: false);
}

private void WriteDynamicHelpContent(string commandName, string parameterName, bool isFullHelp)
Expand Down
3 changes: 3 additions & 0 deletions PSReadLine/KeyBindings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ void SetDefaultWindowsBindings()
{ Keys.Alt9, MakeKeyHandler(DigitArgument, "DigitArgument") },
{ Keys.AltMinus, MakeKeyHandler(DigitArgument, "DigitArgument") },
{ Keys.AltQuestion, MakeKeyHandler(WhatIsKey, "WhatIsKey") },
{ Keys.AltA, MakeKeyHandler(SelectCommandArgument, "SelectCommandArgument") },
{ Keys.F2, MakeKeyHandler(SwitchPredictionView, "SwitchPredictionView") },
{ Keys.F3, MakeKeyHandler(CharacterSearch, "CharacterSearch") },
{ Keys.ShiftF3, MakeKeyHandler(CharacterSearchBackward, "CharacterSearchBackward") },
Expand Down Expand Up @@ -334,6 +335,7 @@ void SetDefaultEmacsBindings()
{ Keys.AltPeriod, MakeKeyHandler(YankLastArg, "YankLastArg") },
{ Keys.AltUnderbar, MakeKeyHandler(YankLastArg, "YankLastArg") },
{ Keys.CtrlAltY, MakeKeyHandler(YankNthArg, "YankNthArg") },
{ Keys.AltA, MakeKeyHandler(SelectCommandArgument,"SelectCommandArgument") },
{ Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") },
{ Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") },
};
Expand Down Expand Up @@ -604,6 +606,7 @@ public static KeyHandlerGroup GetDisplayGrouping(string function)
case nameof(SelectShellBackwardWord):
case nameof(SelectShellForwardWord):
case nameof(SelectShellNextWord):
case nameof(SelectCommandArgument):
return KeyHandlerGroup.Selection;

default:
Expand Down
196 changes: 194 additions & 2 deletions PSReadLine/KillYank.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation.Language;
using Microsoft.PowerShell.Internal;

Expand Down Expand Up @@ -347,9 +348,9 @@ public static void YankLastArg(ConsoleKeyInfo? key = null, object arg = null)
}
}

private void VisualSelectionCommon(Action action)
private void VisualSelectionCommon(Action action, bool forceSetMark = false)
{
if (_singleton._visualSelectionCommandCount == 0)
if (_singleton._visualSelectionCommandCount == 0 || forceSetMark)
{
SetMark();
}
Expand Down Expand Up @@ -449,6 +450,197 @@ public static void SelectBackwardsLine(ConsoleKeyInfo? key = null, object arg =
_singleton.VisualSelectionCommon(() => BeginningOfLine(key, arg));
}

/// <summary>
/// Select the command argument that the cursor is at, or the previous/next Nth command arguments from the current cursor position.
/// </summary>
public static void SelectCommandArgument(ConsoleKeyInfo? key = null, object arg = null)
{
if (!TryGetArgAsInt(arg, out var numericArg, 0))
{
return;
}

_singleton.MaybeParseInput();

int cursor = _singleton._current;
int prev = -1, curr = -1, next = -1;
var sbAsts = _singleton._ast.FindAll(GetScriptBlockAst, searchNestedScriptBlocks: true).ToList();
var arguments = new List<ExpressionAst>();

// We start searching for command arguments from the most nested script block.
for (int i = sbAsts.Count - 1; i >= 0; i --)
{
var sbAst = sbAsts[i];
var cmdAsts = sbAst.FindAll(ast => ast is CommandAst, searchNestedScriptBlocks: false);

foreach (CommandAst cmdAst in cmdAsts)
{
for (int j = 1; j < cmdAst.CommandElements.Count; j++)
{
var argument = cmdAst.CommandElements[j] switch
{
CommandParameterAst paramAst => paramAst.Argument,
ExpressionAst expAst => expAst,
_ => null,
};

if (argument is not null)
{
arguments.Add(argument);

int start = argument.Extent.StartOffset;
int end = argument.Extent.EndOffset;

if (end <= cursor)
{
prev = arguments.Count - 1;
}
if (curr == -1 && start <= cursor && end > cursor)
{
curr = arguments.Count - 1;
}
else if (next == -1 && start > cursor)
{
next = arguments.Count - 1;
}
}
}
}

// Stop searching the outer script blocks if we find any command arguments within the current script block.
if (arguments.Count > 0)
{
break;
}
}

// Simply return if we didn't find any command arguments.
int count = arguments.Count;
if (count == 0)
{
return;
}

if (prev == -1) { prev = count - 1; }
if (next == -1) { next = 0; }
if (curr == -1) { curr = numericArg > 0 ? prev : next; }

int newStartCursor, newEndCursor;
int selectCount = _singleton._visualSelectionCommandCount;

// When an argument is already visually selected by the previous run of this function, the cursor would have past the selected argument.
// In this case, if a user wants to move backward to an argument that is before the currently selected argument by having numericArg < 0,
// we will need to adjust 'numericArg' to move to the expected argument.
// Scenario:
// 1) 'Alt+a' to select an argument;
// 2) 'Alt+-' to make 'numericArg = -1';
// 3) 'Alt+a' to select the argument that is right before the currently selected argument.
if (count > 1 && numericArg < 0 && curr == next && selectCount > 0)
{
var prevArg = arguments[prev];
if (_singleton._mark == prevArg.Extent.StartOffset && cursor == prevArg.Extent.EndOffset)
{
numericArg--;
}
}

while (true)
{
ExpressionAst targetAst = null;
if (numericArg == 0)
{
targetAst = arguments[curr];
}
else
{
int index = curr + numericArg;
index = index >= 0 ? index % count : (count + index % count) % count;
targetAst = arguments[index];
}

// Handle quoted-string arguments specially, by leaving the quotes out of the visual selection.
StringConstantType? constantType = null;
if (targetAst is StringConstantExpressionAst conString)
{
constantType = conString.StringConstantType;
}
else if (targetAst is ExpandableStringExpressionAst expString)
{
constantType = expString.StringConstantType;
}

int startOffsetAdjustment = 0, endOffsetAdjustment = 0;
switch (constantType)
{
case StringConstantType.DoubleQuoted:
case StringConstantType.SingleQuoted:
startOffsetAdjustment = endOffsetAdjustment = 1;
break;
case StringConstantType.DoubleQuotedHereString:
case StringConstantType.SingleQuotedHereString:
startOffsetAdjustment = 2;
endOffsetAdjustment = 3;
break;
default: break;
}

newStartCursor = targetAst.Extent.StartOffset + startOffsetAdjustment;
newEndCursor = targetAst.Extent.EndOffset - endOffsetAdjustment;

// For quoted-string arguments, due to the special handling above, the cursor would always be
// within the selected argument (cursor is placed at the ending quote), and thus when running
// the 'SelectCommandArgument' action again, the same argument would be chosen.
//
// Below is how we detect this and move to the next argument when there is one:
// * the previous action was a visual selection command and the visual range was exactly
// what we are going to make. AND
// * count > 1, meaning that there are other arguments. AND
// * numericArg == 0. When 'numericArg' is not 0, the user is leaping among the available
// arguments, so it's possible that the same argument gets chosen.
// In this case, we should select the next argument.
if (numericArg == 0 && count > 1 && selectCount > 0 &&
_singleton._mark == newStartCursor && cursor == newEndCursor)
{
curr = next;
continue;
}

break;
}

// Move cursor to the start of the argument.
SetCursorPosition(newStartCursor);
// Make the intended range visually selected.
_singleton.VisualSelectionCommon(() => SetCursorPosition(newEndCursor), forceSetMark: true);


// Get the script block AST's whose extent contains the cursor.
bool GetScriptBlockAst(Ast ast)
{
if (ast is not ScriptBlockAst)
{
return false;
}

if (ast.Parent is null)
{
return true;
}

if (ast.Extent.StartOffset >= cursor)
{
return false;
}

// If the script block is closed, then we want the script block only if the cursor is within the script block.
// Otherwise, if the script block is not completed, then we want the script block even if the cursor is at the end.
int textLength = ast.Extent.Text.Length;
return ast.Extent.Text[textLength - 1] == '}'
? ast.Extent.EndOffset - 1 > cursor
: ast.Extent.EndOffset >= cursor;
}
}

/// <summary>
/// Paste text from the system clipboard.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion PSReadLine/PSReadLine.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
<TargetFrameworks>net461;net5.0</TargetFrameworks>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
<LangVersion>8.0</LangVersion>
<LangVersion>9.0</LangVersion>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net461'">
Expand Down
44 changes: 44 additions & 0 deletions PSReadLine/PSReadLineResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion PSReadLine/PSReadLineResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,7 @@ Or not saving history with:
<value>Select the previous suggestion item shown in the list view.</value>
</data>
<data name="SwitchPredictionViewDescription" xml:space="preserve">
<value>Switch to the other prediction view.</value>
<value>Switch between the inline and list prediction views.</value>
</data>
<data name="WindowSizeTooSmallForListView" xml:space="preserve">
<value>The prediction 'ListView' is temporarily disabled because the current window size of the console is too small. To use the 'ListView', please make sure the 'WindowWidth' is not less than '{0}' and the 'WindowHeight' is not less than '{1}'.</value>
Expand All @@ -853,4 +853,7 @@ Or not saving history with:
<data name="ShowParameterHelpDescription" xml:space="preserve">
<value>Shows help for the parameter at the cursor.</value>
</data>
<data name="SelectCommandArgumentDescription" xml:space="preserve">
<value>Make visual selection of the command arguments.</value>
</data>
</root>
Loading