From 5f0459232c1548b7d2025ffb05a022387147ad44 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Wed, 17 Feb 2021 13:43:08 -0800 Subject: [PATCH] Add the 'SelectCommandArgument' bindable function --- PSReadLine/DynamicHelp.cs | 66 +-- PSReadLine/KeyBindings.cs | 3 + PSReadLine/KillYank.cs | 196 +++++++- PSReadLine/PSReadLine.csproj | 2 +- PSReadLine/PSReadLineResources.Designer.cs | 44 ++ PSReadLine/PSReadLineResources.resx | 5 +- PSReadLine/PublicAPI.cs | 2 +- test/KillYankTest.cs | 537 ++++++++++++++++++++- 8 files changed, 814 insertions(+), 41 deletions(-) diff --git a/PSReadLine/DynamicHelp.cs b/PSReadLine/DynamicHelp.cs index d57eadae..b59bd15a 100644 --- a/PSReadLine/DynamicHelp.cs +++ b/PSReadLine/DynamicHelp.cs @@ -15,37 +15,11 @@ namespace Microsoft.PowerShell { public partial class PSConsoleReadLine { - private Microsoft.PowerShell.Pager _pager; - - /// - /// Attempt to show help content. - /// Show the full help for the command on the alternate screen buffer. - /// - public static void ShowCommandHelp(ConsoleKeyInfo? key = null, object arg = null) - { - if (_singleton._console is PlatformWindows.LegacyWin32Console) - { - Collection helpBlock = new Collection() - { - string.Empty, - PSReadLineResources.FullHelpNotSupportedInLegacyConsole - }; - - _singleton.WriteDynamicHelpBlock(helpBlock); - - return; - } - - _singleton.DynamicHelpImpl(isFullHelp: true); - } - - /// - /// Attempt to show help content. - /// Show the short help of the parameter next to the cursor. - /// - 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] @@ -112,9 +86,37 @@ object IPSConsoleReadLineMockableMethods.GetDynamicHelpContent(string commandNam } } - void IPSConsoleReadLineMockableMethods.RenderFullHelp(string content, string regexPatternToScrollTo) + private Pager _pager; + + /// + /// Attempt to show help content. + /// Show the full help for the command on the alternate screen buffer. + /// + public static void ShowCommandHelp(ConsoleKeyInfo? key = null, object arg = null) { - _pager.Write(content, regexPatternToScrollTo); + if (_singleton._console is PlatformWindows.LegacyWin32Console) + { + Collection helpBlock = new Collection() + { + string.Empty, + PSReadLineResources.FullHelpNotSupportedInLegacyConsole + }; + + _singleton.WriteDynamicHelpBlock(helpBlock); + + return; + } + + _singleton.DynamicHelpImpl(isFullHelp: true); + } + + /// + /// Attempt to show help content. + /// Show the short help of the parameter next to the cursor. + /// + public static void ShowParameterHelp(ConsoleKeyInfo? key = null, object arg = null) + { + _singleton.DynamicHelpImpl(isFullHelp: false); } private void WriteDynamicHelpContent(string commandName, string parameterName, bool isFullHelp) diff --git a/PSReadLine/KeyBindings.cs b/PSReadLine/KeyBindings.cs index e1b64d9e..b59449e3 100644 --- a/PSReadLine/KeyBindings.cs +++ b/PSReadLine/KeyBindings.cs @@ -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") }, @@ -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") }, }; @@ -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: diff --git a/PSReadLine/KillYank.cs b/PSReadLine/KillYank.cs index 179bb360..f05fc8ba 100644 --- a/PSReadLine/KillYank.cs +++ b/PSReadLine/KillYank.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Management.Automation.Language; using Microsoft.PowerShell.Internal; @@ -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(); } @@ -449,6 +450,197 @@ public static void SelectBackwardsLine(ConsoleKeyInfo? key = null, object arg = _singleton.VisualSelectionCommon(() => BeginningOfLine(key, arg)); } + /// + /// Select the command argument that the cursor is at, or the previous/next Nth command arguments from the current cursor position. + /// + 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(); + + // 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; + } + } + /// /// Paste text from the system clipboard. /// diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj index 2dfd2a7e..a0162890 100644 --- a/PSReadLine/PSReadLine.csproj +++ b/PSReadLine/PSReadLine.csproj @@ -11,7 +11,7 @@ true net461;net5.0 true - 8.0 + 9.0 diff --git a/PSReadLine/PSReadLineResources.Designer.cs b/PSReadLine/PSReadLineResources.Designer.cs index d302c211..bde0f9e3 100644 --- a/PSReadLine/PSReadLineResources.Designer.cs +++ b/PSReadLine/PSReadLineResources.Designer.cs @@ -2147,5 +2147,49 @@ internal static string ShowParameterHelpDescription return ResourceManager.GetString("ShowParameterHelpDescription", resourceCulture); } } + + /// + /// Looks up a localized string similar to Select the next suggestion item shown in the list view. + /// + internal static string NextSuggestionDescription + { + get + { + return ResourceManager.GetString("NextSuggestionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select the previous suggestion item shown in the list view. + /// + internal static string PreviousSuggestionDescription + { + get + { + return ResourceManager.GetString("PreviousSuggestionDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Switch between the inline and list prediction views. + /// + internal static string SwitchPredictionViewDescription + { + get + { + return ResourceManager.GetString("SwitchPredictionViewDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Make visual selection of the command arguments. + /// + internal static string SelectCommandArgumentDescription + { + get + { + return ResourceManager.GetString("SelectCommandArgumentDescription", resourceCulture); + } + } } } diff --git a/PSReadLine/PSReadLineResources.resx b/PSReadLine/PSReadLineResources.resx index fb75af65..1d042bda 100644 --- a/PSReadLine/PSReadLineResources.resx +++ b/PSReadLine/PSReadLineResources.resx @@ -836,7 +836,7 @@ Or not saving history with: Select the previous suggestion item shown in the list view. - Switch to the other prediction view. + Switch between the inline and list prediction views. 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}'. @@ -853,4 +853,7 @@ Or not saving history with: Shows help for the parameter at the cursor. + + Make visual selection of the command arguments. + diff --git a/PSReadLine/PublicAPI.cs b/PSReadLine/PublicAPI.cs index 7a932661..945c45a3 100644 --- a/PSReadLine/PublicAPI.cs +++ b/PSReadLine/PublicAPI.cs @@ -29,8 +29,8 @@ public interface IPSConsoleReadLineMockableMethods Task> PredictInput(Ast ast, Token[] tokens); void OnCommandLineAccepted(IReadOnlyList history); void OnSuggestionAccepted(Guid predictorId, string suggestionText); - object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp); void RenderFullHelp(string content, string regexPatternToScrollTo); + object GetDynamicHelpContent(string commandName, string parameterName, bool isFullHelp); } [SuppressMessage("Microsoft.MSInternal", "CA903:InternalNamespaceShouldNotContainPublicTypes")] diff --git a/test/KillYankTest.cs b/test/KillYankTest.cs index 1612e7ce..3433f7da 100644 --- a/test/KillYankTest.cs +++ b/test/KillYankTest.cs @@ -506,16 +506,545 @@ public void SelectAll() "echo foo", _.Ctrl_a, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, Selected("echo foo"))), - _.Delete - )); + _.Delete)); Test("", Keys( "echo foo", _.Ctrl_LeftArrow, _.Ctrl_a, CheckThat(() => AssertScreenIs(1, TokenClassification.Command, Selected("echo foo"))), CheckThat(() => AssertCursorLeftIs(8)), - _.Delete - )); + _.Delete)); + } + + public void Debug() + { + while (!System.Diagnostics.Debugger.IsAttached) + { + System.Threading.Thread.Sleep(200); + } + System.Diagnostics.Debugger.Break(); + } + + [SkippableFact] + public void SelectCommandArgument_VariousArgs() + { + TestSetup(KeyMode.Cmd); + + Test("", Keys( + "Test-Sca abc -p1:'a1' \"a2\" -p2 $false", + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, ' ', + TokenClassification.Selection, "abc", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p1:", + TokenClassification.String, "'a1'", + TokenClassification.None, ' ', + TokenClassification.String, "\"a2\"", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p2", + TokenClassification.None, ' ', + TokenClassification.Variable, "$false")), + + // For single/double quoted strings, quotes are left out of selection. + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.String, '\'', + TokenClassification.Selection, "a1", + TokenClassification.String, '\'', + TokenClassification.None, ' ', + TokenClassification.String, "\"a2\"", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p2", + TokenClassification.None, ' ', + TokenClassification.Variable, "$false")), + + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.String, "'a1'", + TokenClassification.None, ' ', + TokenClassification.String, '"', + TokenClassification.Selection, "a2", + TokenClassification.String, '"', + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p2", + TokenClassification.None, ' ', + TokenClassification.Variable, "$false")), + + // Any expression argument can be selected, here we test with a varaible. + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.String, "'a1'", + TokenClassification.None, ' ', + TokenClassification.String, "\"a2\"", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p2", + TokenClassification.None, ' ', + TokenClassification.Selection, "$false")), + + // Verify that we can loop through the arguments. + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, ' ', + TokenClassification.Selection, "abc", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p1:", + TokenClassification.String, "'a1'", + TokenClassification.None, ' ', + TokenClassification.String, "\"a2\"", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p2", + TokenClassification.None, ' ', + TokenClassification.Variable, "$false")), + + // Verify that we can continue to do visual selection. + _.Shift_RightArrow, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, ' ', + TokenClassification.Selection, "abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.String, "'a1'", + TokenClassification.None, ' ', + TokenClassification.String, "\"a2\"", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p2", + TokenClassification.None, ' ', + TokenClassification.Variable, "$false")), + _.Escape)); + } + + [SkippableFact] + public void SelectCommandArgument_HereStringArgs() + { + TestSetup(KeyMode.Cmd); + + Test("", Keys( + "& Test-Sca a1 @'\nabc\n'@ -p1 \"$false\"", + // Command name or command expression should be skipped. + _.Alt_a, CheckThat(() => AssertScreenIs(3, + TokenClassification.None, "& ", + TokenClassification.Command, "Test-Sca", + TokenClassification.None, ' ', + TokenClassification.Selection, "a1", + TokenClassification.None, ' ', + TokenClassification.String, "@'", + NextLine, + TokenClassification.None, ">> ", + TokenClassification.String, "abc", + NextLine, + TokenClassification.None, ">> ", + TokenClassification.String, "'@", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p1", + TokenClassification.None, ' ', + TokenClassification.String, '"', + TokenClassification.Variable, "$false", + TokenClassification.String, '"')), + + // The here-string quotes should be left out of the selection. + _.Alt_a, CheckThat(() => AssertScreenIs(3, + TokenClassification.None, "& ", + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " a1 ", + TokenClassification.String, "@'", + NextLine, + TokenClassification.None, ">> ", + TokenClassification.Selection, "abc", + NextLine, + TokenClassification.None, ">> ", + TokenClassification.String, "'@", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p1", + TokenClassification.None, ' ', + TokenClassification.String, '"', + TokenClassification.Variable, "$false", + TokenClassification.String, '"')), + + // The quotes for an expandable string should be left out of the selection. + _.Alt_a, CheckThat(() => AssertScreenIs(3, + TokenClassification.None, "& ", + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " a1 ", + TokenClassification.String, "@'", + NextLine, + TokenClassification.None, ">> ", + TokenClassification.String, "abc", + NextLine, + TokenClassification.None, ">> ", + TokenClassification.String, "'@", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p1", + TokenClassification.None, ' ', + TokenClassification.String, '"', + TokenClassification.Selection, "$false", + TokenClassification.String, '"')), + + // Loop through arguments and get back to the first argument. + _.Alt_a, CheckThat(() => AssertScreenIs(3, + TokenClassification.None, "& ", + TokenClassification.Command, "Test-Sca", + TokenClassification.None, ' ', + TokenClassification.Selection, "a1", + TokenClassification.None, ' ', + TokenClassification.String, "@'", + NextLine, + TokenClassification.None, ">> ", + TokenClassification.String, "abc", + NextLine, + TokenClassification.None, ">> ", + TokenClassification.String, "'@", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p1", + TokenClassification.None, ' ', + TokenClassification.String, '"', + TokenClassification.Variable, "$false", + TokenClassification.String, '"')), + + // Use digit argument to forward leap 3 arguments. + _.Alt_3, + _.Alt_a, CheckThat(() => AssertScreenIs(3, + TokenClassification.None, "& ", + TokenClassification.Command, "Test-Sca", + TokenClassification.None, ' ', + TokenClassification.Selection, "a1", + TokenClassification.None, ' ', + TokenClassification.String, "@'", + NextLine, + TokenClassification.None, ">> ", + TokenClassification.String, "abc", + NextLine, + TokenClassification.None, ">> ", + TokenClassification.String, "'@", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p1", + TokenClassification.None, ' ', + TokenClassification.String, '"', + TokenClassification.Variable, "$false", + TokenClassification.String, '"')), + + // Use digit argument to forward leap 2 arguments. + _.Alt_2, + _.Alt_a, CheckThat(() => AssertScreenIs(3, + TokenClassification.None, "& ", + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " a1 ", + TokenClassification.String, "@'", + NextLine, + TokenClassification.None, ">> ", + TokenClassification.String, "abc", + NextLine, + TokenClassification.None, ">> ", + TokenClassification.String, "'@", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p1", + TokenClassification.None, ' ', + TokenClassification.String, '"', + TokenClassification.Selection, "$false", + TokenClassification.String, '"')), + _.Escape)); + } + + [SkippableFact] + public void SelectCommandArgument_NestedScriptBlock() + { + TestSetup(KeyMode.Cmd); + + Test("", Keys( + "Test-Sca abc -p1:a1 { Get-Command cmd -Module xxx } -p2 a2", + + // Loop through all arguments. + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, ' ', + TokenClassification.Selection, "abc", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p1:", + TokenClassification.None, "a1 { ", + TokenClassification.Command, "Get-Command", + TokenClassification.None, " cmd ", + TokenClassification.Parameter, "-Module", + TokenClassification.None, " xxx } ", + TokenClassification.Parameter, "-p2", + TokenClassification.None, " a2")), + + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.Selection, "a1", + TokenClassification.None, " { ", + TokenClassification.Command, "Get-Command", + TokenClassification.None, " cmd ", + TokenClassification.Parameter, "-Module", + TokenClassification.None, " xxx } ", + TokenClassification.Parameter, "-p2", + TokenClassification.None, " a2")), + + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.None, "a1 ", + TokenClassification.Selection, "{ Get-Command cmd -Module xxx }", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p2", + TokenClassification.None, " a2")), + + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.None, "a1 { ", + TokenClassification.Command, "Get-Command", + TokenClassification.None, " cmd ", + TokenClassification.Parameter, "-Module", + TokenClassification.None, " xxx } ", + TokenClassification.Parameter, "-p2", + TokenClassification.None, ' ', + TokenClassification.Selection, "a2")), + + // Forward leap 3 arguments, to select the script block argument. + _.Alt_3, + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.None, "a1 ", + TokenClassification.Selection, "{ Get-Command cmd -Module xxx }", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p2", + TokenClassification.None, " a2")), + CheckThat(() => AssertCursorLeftIs(51)), + + // Backward leap 3 arguments. + _.Alt_Minus, _.Alt_3, + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.None, "a1 { ", + TokenClassification.Command, "Get-Command", + TokenClassification.None, " cmd ", + TokenClassification.Parameter, "-Module", + TokenClassification.None, " xxx } ", + TokenClassification.Parameter, "-p2", + TokenClassification.None, ' ', + TokenClassification.Selection, "a2")), + CheckThat(() => AssertCursorLeftIs(58)), + + // One more backward leap to again select the script block argument. + _.Alt_Minus, + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.None, "a1 ", + TokenClassification.Selection, "{ Get-Command cmd -Module xxx }", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p2", + TokenClassification.None, " a2")), + CheckThat(() => AssertCursorLeftIs(51)), + + // Move cursor to be inside the script block. + _.LeftArrow, + _.LeftArrow, + + // Now we should loop through the command arguments within that script block. + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.None, "a1 { ", + TokenClassification.Command, "Get-Command", + TokenClassification.None, ' ', + TokenClassification.Selection, "cmd", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-Module", + TokenClassification.None, " xxx } ", + TokenClassification.Parameter, "-p2", + TokenClassification.None, " a2")), + + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.None, "a1 { ", + TokenClassification.Command, "Get-Command", + TokenClassification.None, " cmd ", + TokenClassification.Parameter, "-Module", + TokenClassification.None, ' ', + TokenClassification.Selection, "xxx", + TokenClassification.None, " } ", + TokenClassification.Parameter, "-p2", + TokenClassification.None, " a2")), + + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " abc ", + TokenClassification.Parameter, "-p1:", + TokenClassification.None, "a1 { ", + TokenClassification.Command, "Get-Command", + TokenClassification.None, ' ', + TokenClassification.Selection, "cmd", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-Module", + TokenClassification.None, " xxx } ", + TokenClassification.Parameter, "-p2", + TokenClassification.None, " a2")), + + _.Escape)); + } + + [SkippableFact] + public void SelectCommandArgument_DigitArgument() + { + TestSetup(KeyMode.Cmd); + + Test("", Keys( + "Test-Sca aaaa bbbb -p cccc", + + // Move the cursor to the first 'a' + CheckThat(() => PSConsoleReadLine.SetCursorPosition(9)), + // Should select 'aaaa' + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, ' ', + TokenClassification.Selection, "aaaa", + TokenClassification.None, " bbbb ", + TokenClassification.Parameter, "-p", + TokenClassification.None, " cccc")), + + // Move the cursor to the second 'b' + CheckThat(() => PSConsoleReadLine.SetCursorPosition(15)), + // Should select 'bbbb' + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " aaaa ", + TokenClassification.Selection, "bbbb", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p", + TokenClassification.None, " cccc")), + + // Move the cursor to the third 'c' + CheckThat(() => PSConsoleReadLine.SetCursorPosition(24)), + // Should select 'cccc' + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " aaaa bbbb ", + TokenClassification.Parameter, "-p", + TokenClassification.None, ' ', + TokenClassification.Selection, "cccc")), + + // Use digit argument '2', meaning forward by 2 arguments. + _.Alt_2, + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " aaaa ", + TokenClassification.Selection, "bbbb", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p", + TokenClassification.None, " cccc")), + CheckThat(() => AssertCursorLeftIs(18)), + + // Use digit argument '-1', meaning backward by 1 argument. + // Since 'bbbb' is currently selected, backwarding by 1 should select 'aaaa'. + _.Alt_Minus, + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, ' ', + TokenClassification.Selection, "aaaa", + TokenClassification.None, " bbbb ", + TokenClassification.Parameter, "-p", + TokenClassification.None, " cccc")), + + // Use digit argument '-2', meaning backward by 2 arguments. + // Since 'aaaa' is currently selected, backwarding by 1 should select 'bbbb'. + _.Alt_Minus, _.Alt_2, + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " aaaa ", + TokenClassification.Selection, "bbbb", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p", + TokenClassification.None, " cccc")), + + // Use digit argument '3', meaning forward by 3 arguments. + // Since 'bbbb' is currently selected, forwarding by 3 should select 'bbbb' again. + _.Alt_3, + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " aaaa ", + TokenClassification.Selection, "bbbb", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p", + TokenClassification.None, " cccc")), + + // Use digit argument '-2', meaning backward by 2 arguments. + // Since 'bbbb' is currently selected, backwarding by 2 should select 'cccc'. + _.Alt_Minus, _.Alt_2, + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " aaaa bbbb ", + TokenClassification.Parameter, "-p", + TokenClassification.None, ' ', + TokenClassification.Selection, "cccc")), + + // Use digit argument '-1', meaning backward by 1 argument. + // Since 'cccc' is currently selected, backwarding by 2 should select 'bbbb'. + _.Alt_Minus, + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " aaaa ", + TokenClassification.Selection, "bbbb", + TokenClassification.None, ' ', + TokenClassification.Parameter, "-p", + TokenClassification.None, " cccc")), + + // Clear the selection, and place cursor at the fourth 'b' + _.LeftArrow, + // Use digit argument '-1', meaning backward by 1 argument. + _.Alt_Minus, + // Since the cursor is currently on the argument 'bbbb', backwarding by 1 should select 'aaaa'. + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, ' ', + TokenClassification.Selection, "aaaa", + TokenClassification.None, " bbbb ", + TokenClassification.Parameter, "-p", + TokenClassification.None, " cccc")), + + // Clear the selection, and place cursor at the space right after 'aaaa' + _.RightArrow, _.LeftArrow, + // Use digit argument '-1', meaning backward by 1 argument. + _.Alt_Minus, + // Since the cursor is between the first and the second arguments, backwarding by 1 should select the first argument 'aaaa'. + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, ' ', + TokenClassification.Selection, "aaaa", + TokenClassification.None, " bbbb ", + TokenClassification.Parameter, "-p", + TokenClassification.None, " cccc")), + + // Clear the selection, and place cursor at the space right after 'aaaa' + _.RightArrow, _.LeftArrow, + // Use digit argument '2', meaning forward by 2 arguments. + _.Alt_2, + // Since the cursor is between the first and the second arguments, forwarding by 2 should select the 3rd argument 'cccc'. + _.Alt_a, CheckThat(() => AssertScreenIs(1, + TokenClassification.Command, "Test-Sca", + TokenClassification.None, " aaaa bbbb ", + TokenClassification.Parameter, "-p", + TokenClassification.None, ' ', + TokenClassification.Selection, "cccc")), + + _.Escape)); } } }