Skip to content

Commit 6ed95a2

Browse files
authored
Add the SelectCommandArgument bind-able function (#2222)
1 parent bbb729c commit 6ed95a2

File tree

8 files changed

+814
-41
lines changed

8 files changed

+814
-41
lines changed

PSReadLine/DynamicHelp.cs

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -15,37 +15,11 @@ namespace Microsoft.PowerShell
1515
{
1616
public partial class PSConsoleReadLine
1717
{
18-
private Microsoft.PowerShell.Pager _pager;
19-
20-
/// <summary>
21-
/// Attempt to show help content.
22-
/// Show the full help for the command on the alternate screen buffer.
23-
/// </summary>
24-
public static void ShowCommandHelp(ConsoleKeyInfo? key = null, object arg = null)
25-
{
26-
if (_singleton._console is PlatformWindows.LegacyWin32Console)
27-
{
28-
Collection<string> helpBlock = new Collection<string>()
29-
{
30-
string.Empty,
31-
PSReadLineResources.FullHelpNotSupportedInLegacyConsole
32-
};
33-
34-
_singleton.WriteDynamicHelpBlock(helpBlock);
35-
36-
return;
37-
}
38-
39-
_singleton.DynamicHelpImpl(isFullHelp: true);
40-
}
41-
42-
/// <summary>
43-
/// Attempt to show help content.
44-
/// Show the short help of the parameter next to the cursor.
45-
/// </summary>
46-
public static void ShowParameterHelp(ConsoleKeyInfo? key = null, object arg = null)
18+
// Stub helper methods so dynamic help can be mocked
19+
[ExcludeFromCodeCoverage]
20+
void IPSConsoleReadLineMockableMethods.RenderFullHelp(string content, string regexPatternToScrollTo)
4721
{
48-
_singleton.DynamicHelpImpl(isFullHelp: false);
22+
_pager.Write(content, regexPatternToScrollTo);
4923
}
5024

5125
[ExcludeFromCodeCoverage]
@@ -112,9 +86,37 @@ object IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandNam
11286
}
11387
}
11488

115-
void IPSConsoleReadLineMockableMethods.RenderFullHelp(string content, string regexPatternToScrollTo)
89+
private Pager _pager;
90+
91+
/// <summary>
92+
/// Attempt to show help content.
93+
/// Show the full help for the command on the alternate screen buffer.
94+
/// </summary>
95+
public static void ShowCommandHelp(ConsoleKeyInfo? key = null, object arg = null)
11696
{
117-
_pager.Write(content, regexPatternToScrollTo);
97+
if (_singleton._console is PlatformWindows.LegacyWin32Console)
98+
{
99+
Collection<string> helpBlock = new Collection<string>()
100+
{
101+
string.Empty,
102+
PSReadLineResources.FullHelpNotSupportedInLegacyConsole
103+
};
104+
105+
_singleton.WriteDynamicHelpBlock(helpBlock);
106+
107+
return;
108+
}
109+
110+
_singleton.DynamicHelpImpl(isFullHelp: true);
111+
}
112+
113+
/// <summary>
114+
/// Attempt to show help content.
115+
/// Show the short help of the parameter next to the cursor.
116+
/// </summary>
117+
public static void ShowParameterHelp(ConsoleKeyInfo? key = null, object arg = null)
118+
{
119+
_singleton.DynamicHelpImpl(isFullHelp: false);
118120
}
119121

120122
private void WriteDynamicHelpContent(string commandName, string parameterName, bool isFullHelp)

PSReadLine/KeyBindings.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ void SetDefaultWindowsBindings()
229229
{ Keys.Alt9, MakeKeyHandler(DigitArgument, "DigitArgument") },
230230
{ Keys.AltMinus, MakeKeyHandler(DigitArgument, "DigitArgument") },
231231
{ Keys.AltQuestion, MakeKeyHandler(WhatIsKey, "WhatIsKey") },
232+
{ Keys.AltA, MakeKeyHandler(SelectCommandArgument, "SelectCommandArgument") },
232233
{ Keys.F2, MakeKeyHandler(SwitchPredictionView, "SwitchPredictionView") },
233234
{ Keys.F3, MakeKeyHandler(CharacterSearch, "CharacterSearch") },
234235
{ Keys.ShiftF3, MakeKeyHandler(CharacterSearchBackward, "CharacterSearchBackward") },
@@ -334,6 +335,7 @@ void SetDefaultEmacsBindings()
334335
{ Keys.AltPeriod, MakeKeyHandler(YankLastArg, "YankLastArg") },
335336
{ Keys.AltUnderbar, MakeKeyHandler(YankLastArg, "YankLastArg") },
336337
{ Keys.CtrlAltY, MakeKeyHandler(YankNthArg, "YankNthArg") },
338+
{ Keys.AltA, MakeKeyHandler(SelectCommandArgument,"SelectCommandArgument") },
337339
{ Keys.AltH, MakeKeyHandler(ShowParameterHelp, "ShowParameterHelp") },
338340
{ Keys.F1, MakeKeyHandler(ShowCommandHelp, "ShowCommandHelp") },
339341
};
@@ -604,6 +606,7 @@ public static KeyHandlerGroup GetDisplayGrouping(string function)
604606
case nameof(SelectShellBackwardWord):
605607
case nameof(SelectShellForwardWord):
606608
case nameof(SelectShellNextWord):
609+
case nameof(SelectCommandArgument):
607610
return KeyHandlerGroup.Selection;
608611

609612
default:

PSReadLine/KillYank.cs

Lines changed: 194 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.Collections.Generic;
7+
using System.Linq;
78
using System.Management.Automation.Language;
89
using Microsoft.PowerShell.Internal;
910

@@ -347,9 +348,9 @@ public static void YankLastArg(ConsoleKeyInfo? key = null, object arg = null)
347348
}
348349
}
349350

350-
private void VisualSelectionCommon(Action action)
351+
private void VisualSelectionCommon(Action action, bool forceSetMark = false)
351352
{
352-
if (_singleton._visualSelectionCommandCount == 0)
353+
if (_singleton._visualSelectionCommandCount == 0 || forceSetMark)
353354
{
354355
SetMark();
355356
}
@@ -449,6 +450,197 @@ public static void SelectBackwardsLine(ConsoleKeyInfo? key = null, object arg =
449450
_singleton.VisualSelectionCommon(() => BeginningOfLine(key, arg));
450451
}
451452

453+
/// <summary>
454+
/// Select the command argument that the cursor is at, or the previous/next Nth command arguments from the current cursor position.
455+
/// </summary>
456+
public static void SelectCommandArgument(ConsoleKeyInfo? key = null, object arg = null)
457+
{
458+
if (!TryGetArgAsInt(arg, out var numericArg, 0))
459+
{
460+
return;
461+
}
462+
463+
_singleton.MaybeParseInput();
464+
465+
int cursor = _singleton._current;
466+
int prev = -1, curr = -1, next = -1;
467+
var sbAsts = _singleton._ast.FindAll(GetScriptBlockAst, searchNestedScriptBlocks: true).ToList();
468+
var arguments = new List<ExpressionAst>();
469+
470+
// We start searching for command arguments from the most nested script block.
471+
for (int i = sbAsts.Count - 1; i >= 0; i --)
472+
{
473+
var sbAst = sbAsts[i];
474+
var cmdAsts = sbAst.FindAll(ast => ast is CommandAst, searchNestedScriptBlocks: false);
475+
476+
foreach (CommandAst cmdAst in cmdAsts)
477+
{
478+
for (int j = 1; j < cmdAst.CommandElements.Count; j++)
479+
{
480+
var argument = cmdAst.CommandElements[j] switch
481+
{
482+
CommandParameterAst paramAst => paramAst.Argument,
483+
ExpressionAst expAst => expAst,
484+
_ => null,
485+
};
486+
487+
if (argument is not null)
488+
{
489+
arguments.Add(argument);
490+
491+
int start = argument.Extent.StartOffset;
492+
int end = argument.Extent.EndOffset;
493+
494+
if (end <= cursor)
495+
{
496+
prev = arguments.Count - 1;
497+
}
498+
if (curr == -1 && start <= cursor && end > cursor)
499+
{
500+
curr = arguments.Count - 1;
501+
}
502+
else if (next == -1 && start > cursor)
503+
{
504+
next = arguments.Count - 1;
505+
}
506+
}
507+
}
508+
}
509+
510+
// Stop searching the outer script blocks if we find any command arguments within the current script block.
511+
if (arguments.Count > 0)
512+
{
513+
break;
514+
}
515+
}
516+
517+
// Simply return if we didn't find any command arguments.
518+
int count = arguments.Count;
519+
if (count == 0)
520+
{
521+
return;
522+
}
523+
524+
if (prev == -1) { prev = count - 1; }
525+
if (next == -1) { next = 0; }
526+
if (curr == -1) { curr = numericArg > 0 ? prev : next; }
527+
528+
int newStartCursor, newEndCursor;
529+
int selectCount = _singleton._visualSelectionCommandCount;
530+
531+
// When an argument is already visually selected by the previous run of this function, the cursor would have past the selected argument.
532+
// In this case, if a user wants to move backward to an argument that is before the currently selected argument by having numericArg < 0,
533+
// we will need to adjust 'numericArg' to move to the expected argument.
534+
// Scenario:
535+
// 1) 'Alt+a' to select an argument;
536+
// 2) 'Alt+-' to make 'numericArg = -1';
537+
// 3) 'Alt+a' to select the argument that is right before the currently selected argument.
538+
if (count > 1 && numericArg < 0 && curr == next && selectCount > 0)
539+
{
540+
var prevArg = arguments[prev];
541+
if (_singleton._mark == prevArg.Extent.StartOffset && cursor == prevArg.Extent.EndOffset)
542+
{
543+
numericArg--;
544+
}
545+
}
546+
547+
while (true)
548+
{
549+
ExpressionAst targetAst = null;
550+
if (numericArg == 0)
551+
{
552+
targetAst = arguments[curr];
553+
}
554+
else
555+
{
556+
int index = curr + numericArg;
557+
index = index >= 0 ? index % count : (count + index % count) % count;
558+
targetAst = arguments[index];
559+
}
560+
561+
// Handle quoted-string arguments specially, by leaving the quotes out of the visual selection.
562+
StringConstantType? constantType = null;
563+
if (targetAst is StringConstantExpressionAst conString)
564+
{
565+
constantType = conString.StringConstantType;
566+
}
567+
else if (targetAst is ExpandableStringExpressionAst expString)
568+
{
569+
constantType = expString.StringConstantType;
570+
}
571+
572+
int startOffsetAdjustment = 0, endOffsetAdjustment = 0;
573+
switch (constantType)
574+
{
575+
case StringConstantType.DoubleQuoted:
576+
case StringConstantType.SingleQuoted:
577+
startOffsetAdjustment = endOffsetAdjustment = 1;
578+
break;
579+
case StringConstantType.DoubleQuotedHereString:
580+
case StringConstantType.SingleQuotedHereString:
581+
startOffsetAdjustment = 2;
582+
endOffsetAdjustment = 3;
583+
break;
584+
default: break;
585+
}
586+
587+
newStartCursor = targetAst.Extent.StartOffset + startOffsetAdjustment;
588+
newEndCursor = targetAst.Extent.EndOffset - endOffsetAdjustment;
589+
590+
// For quoted-string arguments, due to the special handling above, the cursor would always be
591+
// within the selected argument (cursor is placed at the ending quote), and thus when running
592+
// the 'SelectCommandArgument' action again, the same argument would be chosen.
593+
//
594+
// Below is how we detect this and move to the next argument when there is one:
595+
// * the previous action was a visual selection command and the visual range was exactly
596+
// what we are going to make. AND
597+
// * count > 1, meaning that there are other arguments. AND
598+
// * numericArg == 0. When 'numericArg' is not 0, the user is leaping among the available
599+
// arguments, so it's possible that the same argument gets chosen.
600+
// In this case, we should select the next argument.
601+
if (numericArg == 0 && count > 1 && selectCount > 0 &&
602+
_singleton._mark == newStartCursor && cursor == newEndCursor)
603+
{
604+
curr = next;
605+
continue;
606+
}
607+
608+
break;
609+
}
610+
611+
// Move cursor to the start of the argument.
612+
SetCursorPosition(newStartCursor);
613+
// Make the intended range visually selected.
614+
_singleton.VisualSelectionCommon(() => SetCursorPosition(newEndCursor), forceSetMark: true);
615+
616+
617+
// Get the script block AST's whose extent contains the cursor.
618+
bool GetScriptBlockAst(Ast ast)
619+
{
620+
if (ast is not ScriptBlockAst)
621+
{
622+
return false;
623+
}
624+
625+
if (ast.Parent is null)
626+
{
627+
return true;
628+
}
629+
630+
if (ast.Extent.StartOffset >= cursor)
631+
{
632+
return false;
633+
}
634+
635+
// If the script block is closed, then we want the script block only if the cursor is within the script block.
636+
// Otherwise, if the script block is not completed, then we want the script block even if the cursor is at the end.
637+
int textLength = ast.Extent.Text.Length;
638+
return ast.Extent.Text[textLength - 1] == '}'
639+
? ast.Extent.EndOffset - 1 > cursor
640+
: ast.Extent.EndOffset >= cursor;
641+
}
642+
}
643+
452644
/// <summary>
453645
/// Paste text from the system clipboard.
454646
/// </summary>

PSReadLine/PSReadLine.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
1212
<TargetFrameworks>net461;net5.0</TargetFrameworks>
1313
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
14-
<LangVersion>8.0</LangVersion>
14+
<LangVersion>9.0</LangVersion>
1515
</PropertyGroup>
1616

1717
<ItemGroup Condition="'$(TargetFramework)' == 'net461'">

PSReadLine/PSReadLineResources.Designer.cs

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

PSReadLine/PSReadLineResources.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -836,7 +836,7 @@ Or not saving history with:
836836
<value>Select the previous suggestion item shown in the list view.</value>
837837
</data>
838838
<data name="SwitchPredictionViewDescription" xml:space="preserve">
839-
<value>Switch to the other prediction view.</value>
839+
<value>Switch between the inline and list prediction views.</value>
840840
</data>
841841
<data name="WindowSizeTooSmallForListView" xml:space="preserve">
842842
<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>
@@ -853,4 +853,7 @@ Or not saving history with:
853853
<data name="ShowParameterHelpDescription" xml:space="preserve">
854854
<value>Shows help for the parameter at the cursor.</value>
855855
</data>
856+
<data name="SelectCommandArgumentDescription" xml:space="preserve">
857+
<value>Make visual selection of the command arguments.</value>
858+
</data>
856859
</root>

0 commit comments

Comments
 (0)