From fb4f642a8284bfc11663273af8289f3dc0e8a51a Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Mon, 11 Jan 2021 22:23:35 +0100 Subject: [PATCH 01/11] Add completion for requires statements. --- .../CommandCompletion/CompletionAnalysis.cs | 2 +- .../CommandCompletion/CompletionCompleters.cs | 150 +++++++++++++++++- 2 files changed, 149 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 95b26ff6907..4479897dfd4 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -473,7 +473,7 @@ internal List GetResultHelper(CompletionContext completionCont break; completionContext.WordToComplete = tokenAtCursor.Text; - result = CompletionCompleters.CompleteComment(completionContext); + result = CompletionCompleters.CompleteComment(completionContext, ref replacementIndex, ref replacementLength); break; case TokenKind.StringExpandable: diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 853b79e7843..8634ba6fd67 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4923,11 +4923,157 @@ private static SortedSet BuildSpecialVariablesCache() #region Comments - // Complete the history entries - internal static List CompleteComment(CompletionContext context) + + internal static List CompleteComment(CompletionContext context, ref int replacementIndex, ref int replacementLength) { List results = new List(); + //Complete #requires statements + if (context.WordToComplete.StartsWith("#requires ", StringComparison.OrdinalIgnoreCase)) + { + int cursorIndex = context.CursorPosition.ColumnNumber - 1; + //cursor is within the "#requires " statement. + if (cursorIndex < 10) + { + return results; + } + + string lineToCursor = context.CursorPosition.Line.Substring(0, cursorIndex); + //RunAsAdministrator must be the last parameter in a Requires statement so no completion if the cursor is after the parameter. + if (lineToCursor.Contains(" -RunAsAdministrator", StringComparison.OrdinalIgnoreCase)) + { + return results; + } + + //Regex to find parameter like " -Parameter1" or " -" + MatchCollection foundMatches = Regex.Matches(lineToCursor, "\\s-([A-Za-z]+|$)"); + if (foundMatches.Count == 0) + { + return results; + } + + string currentParameter = foundMatches[^1].Groups[1].Value; + //Complete the parameter if the cursor is at a parameter + if (lineToCursor.LastIndexOf($"-{currentParameter}") + currentParameter.Length + 1 == cursorIndex) + { + replacementIndex = cursorIndex - currentParameter.Length; + replacementLength = currentParameter.Length; + + var requiresParameters = new Tuple[4] { + new Tuple("Modules","Specifies PowerShell modules that the script requires."), + new Tuple("PSEdition","Specifies a PowerShell edition that the script requires."), + new Tuple("RunAsAdministrator","Specifies that PowerShell must be running as administrator on Windows."), + new Tuple("Version","Specifies the minimum version of PowerShell that the script requires.") + }; + foreach (var parameter in requiresParameters) + { + if (context.CursorPosition.Line.Contains($" -{parameter.Item1}", StringComparison.OrdinalIgnoreCase) == false && + parameter.Item1.StartsWith(currentParameter, StringComparison.OrdinalIgnoreCase)) + { + results.Add(new CompletionResult(parameter.Item1, parameter.Item1, CompletionResultType.ParameterName, parameter.Item2)); + } + } + } + //Complete parameter values + else + { + //Regex to find parameter values (any text that appears after various delimiters) + foundMatches = Regex.Matches(lineToCursor, "(\\s|,|;|{|\"|'|=)(\\w+|$)"); + string currentValue; + if (foundMatches.Count == 0) + { + currentValue = string.Empty; + } + else + { + currentValue = foundMatches[^1].Groups[^1].Value; + } + + replacementIndex = cursorIndex - currentValue.Length; + replacementLength = currentValue.Length; + + if (currentParameter.Equals("PSEdition", StringComparison.OrdinalIgnoreCase)) + { + var psEditionValues = new Tuple[2] { + new Tuple("Core","Specifies that the script requires PowerShell Core to run."), + new Tuple("Desktop","Specifies that the script requires Windows PowerShell to run.") + }; + foreach (var value in psEditionValues) + { + if (value.Item1.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase)) + { + results.Add(new CompletionResult(value.Item1, value.Item1, CompletionResultType.ParameterValue, value.Item2)); + } + } + } + + if (currentParameter.Equals("Modules", StringComparison.OrdinalIgnoreCase)) + { + int hashtableStart = lineToCursor.LastIndexOf("@{"); + int hashtableEnd = lineToCursor.LastIndexOf('}'); + + //Cursor is inside a hashtable + if (hashtableStart != -1 && hashtableEnd == -1 || hashtableEnd < hashtableStart) + { + string hashtableString = lineToCursor.Substring(hashtableStart); + //Regex to find hashtable keys with or without quotes + foundMatches = Regex.Matches(hashtableString, "(@{|;)\\s*(?:'|\"|\\w*)\\w*"); + var hashtableKeys = new string[foundMatches.Count]; + for (int i = 0; i < hashtableKeys.Length; i++) + { + hashtableKeys[i] = foundMatches[i].Value.TrimStart('@', '{', ';', '"', '\''); + } + var lastCaptureGroup = foundMatches[^1].Groups[0]; + + //Are we completing a key for the hashtable? + if (lastCaptureGroup.Index + lastCaptureGroup.Length == hashtableString.Length) + { + var moduleKeys = new Tuple[2] { + new Tuple("ModuleName","Required. Specifies the module name."), + new Tuple("GUID","Optional. Specifies the GUID of the module.") + }; + //The following keys cannot be used together so they get their own table for lookup. + var moduleVersionKeys = new Dictionary(3, StringComparer.OrdinalIgnoreCase) { + {"ModuleVersion", "Specifies a minimum acceptable version of the module." }, + {"RequiredVersion", "Specifies an exact, required version of the module." }, + {"MaximumVersion", "Specifies the maximum acceptable version of the module." }, + }; + foreach (var value in moduleKeys) + { + if (value.Item1.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) && hashtableKeys.Contains(value.Item1) == false) + { + results.Add(new CompletionResult(value.Item1, value.Item1, CompletionResultType.ParameterValue, value.Item2)); + } + } + + foreach (string value in moduleVersionKeys.Keys) + { + if (value.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) && moduleVersionKeys.Keys.Intersect(hashtableKeys).Any() == false) + { + results.Add(new CompletionResult(value, value, CompletionResultType.ParameterValue, moduleVersionKeys[value])); + } + } + } + else + { + if (hashtableKeys[^1].Equals("ModuleName", StringComparison.OrdinalIgnoreCase)) + { + context.WordToComplete = currentValue; + return CompleteModuleName(context, true); + } + } + } + else + { + context.WordToComplete = currentValue; + return CompleteModuleName(context, true); + } + } + } + return results; + } + + // Complete the history entries Match matchResult = Regex.Match(context.WordToComplete, @"^#([\w\-]*)$"); if (!matchResult.Success) { return results; } From cf3eaaac65fe742ef1e0ab85849e86f958e71bd0 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Mon, 11 Jan 2021 23:03:55 +0100 Subject: [PATCH 02/11] Fix offset when requires isn't on first line. --- .../engine/CommandCompletion/CompletionCompleters.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 8634ba6fd67..c35e83fedd4 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4956,7 +4956,7 @@ internal static List CompleteComment(CompletionContext context //Complete the parameter if the cursor is at a parameter if (lineToCursor.LastIndexOf($"-{currentParameter}") + currentParameter.Length + 1 == cursorIndex) { - replacementIndex = cursorIndex - currentParameter.Length; + replacementIndex = context.CursorPosition.Offset - currentParameter.Length; replacementLength = currentParameter.Length; var requiresParameters = new Tuple[4] { @@ -4989,7 +4989,7 @@ internal static List CompleteComment(CompletionContext context currentValue = foundMatches[^1].Groups[^1].Value; } - replacementIndex = cursorIndex - currentValue.Length; + replacementIndex = context.CursorPosition.Offset - currentValue.Length; replacementLength = currentValue.Length; if (currentParameter.Equals("PSEdition", StringComparison.OrdinalIgnoreCase)) From b8081eb0394c861125dde9ec9e94f024de40397f Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Wed, 13 Jan 2021 07:04:17 +0100 Subject: [PATCH 03/11] Fix various style issues. --- .../CommandCompletion/CompletionCompleters.cs | 65 +++++++++++-------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index c35e83fedd4..8edf5e8b5c3 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4923,29 +4923,30 @@ private static SortedSet BuildSpecialVariablesCache() #region Comments - internal static List CompleteComment(CompletionContext context, ref int replacementIndex, ref int replacementLength) { List results = new List(); - //Complete #requires statements + // Complete #requires statements if (context.WordToComplete.StartsWith("#requires ", StringComparison.OrdinalIgnoreCase)) { int cursorIndex = context.CursorPosition.ColumnNumber - 1; - //cursor is within the "#requires " statement. + + // cursor is within the "#requires " statement. if (cursorIndex < 10) { return results; } string lineToCursor = context.CursorPosition.Line.Substring(0, cursorIndex); - //RunAsAdministrator must be the last parameter in a Requires statement so no completion if the cursor is after the parameter. + + // RunAsAdministrator must be the last parameter in a Requires statement so no completion if the cursor is after the parameter. if (lineToCursor.Contains(" -RunAsAdministrator", StringComparison.OrdinalIgnoreCase)) { return results; } - //Regex to find parameter like " -Parameter1" or " -" + // Regex to find parameter like " -Parameter1" or " -" MatchCollection foundMatches = Regex.Matches(lineToCursor, "\\s-([A-Za-z]+|$)"); if (foundMatches.Count == 0) { @@ -4953,17 +4954,19 @@ internal static List CompleteComment(CompletionContext context } string currentParameter = foundMatches[^1].Groups[1].Value; - //Complete the parameter if the cursor is at a parameter + + // Complete the parameter if the cursor is at a parameter if (lineToCursor.LastIndexOf($"-{currentParameter}") + currentParameter.Length + 1 == cursorIndex) { replacementIndex = context.CursorPosition.Offset - currentParameter.Length; replacementLength = currentParameter.Length; - var requiresParameters = new Tuple[4] { - new Tuple("Modules","Specifies PowerShell modules that the script requires."), - new Tuple("PSEdition","Specifies a PowerShell edition that the script requires."), - new Tuple("RunAsAdministrator","Specifies that PowerShell must be running as administrator on Windows."), - new Tuple("Version","Specifies the minimum version of PowerShell that the script requires.") + var requiresParameters = new Tuple[4] + { + new Tuple("Modules", "Specifies PowerShell modules that the script requires."), + new Tuple("PSEdition", "Specifies a PowerShell edition that the script requires."), + new Tuple("RunAsAdministrator", "Specifies that PowerShell must be running as administrator on Windows."), + new Tuple("Version", "Specifies the minimum version of PowerShell that the script requires.") }; foreach (var parameter in requiresParameters) { @@ -4974,10 +4977,9 @@ internal static List CompleteComment(CompletionContext context } } } - //Complete parameter values else { - //Regex to find parameter values (any text that appears after various delimiters) + // Regex to find parameter values (any text that appears after various delimiters) foundMatches = Regex.Matches(lineToCursor, "(\\s|,|;|{|\"|'|=)(\\w+|$)"); string currentValue; if (foundMatches.Count == 0) @@ -4994,9 +4996,10 @@ internal static List CompleteComment(CompletionContext context if (currentParameter.Equals("PSEdition", StringComparison.OrdinalIgnoreCase)) { - var psEditionValues = new Tuple[2] { - new Tuple("Core","Specifies that the script requires PowerShell Core to run."), - new Tuple("Desktop","Specifies that the script requires Windows PowerShell to run.") + var psEditionValues = new Tuple[2] + { + new Tuple("Core", "Specifies that the script requires PowerShell Core to run."), + new Tuple("Desktop", "Specifies that the script requires Windows PowerShell to run.") }; foreach (var value in psEditionValues) { @@ -5012,31 +5015,36 @@ internal static List CompleteComment(CompletionContext context int hashtableStart = lineToCursor.LastIndexOf("@{"); int hashtableEnd = lineToCursor.LastIndexOf('}'); - //Cursor is inside a hashtable - if (hashtableStart != -1 && hashtableEnd == -1 || hashtableEnd < hashtableStart) + // Cursor is inside a hashtable + if (hashtableStart != -1 && (hashtableEnd == -1 || hashtableEnd < hashtableStart)) { string hashtableString = lineToCursor.Substring(hashtableStart); - //Regex to find hashtable keys with or without quotes + + // Regex to find hashtable keys with or without quotes foundMatches = Regex.Matches(hashtableString, "(@{|;)\\s*(?:'|\"|\\w*)\\w*"); var hashtableKeys = new string[foundMatches.Count]; for (int i = 0; i < hashtableKeys.Length; i++) { hashtableKeys[i] = foundMatches[i].Value.TrimStart('@', '{', ';', '"', '\''); } + var lastCaptureGroup = foundMatches[^1].Groups[0]; - //Are we completing a key for the hashtable? + // Are we completing a key for the hashtable? if (lastCaptureGroup.Index + lastCaptureGroup.Length == hashtableString.Length) { - var moduleKeys = new Tuple[2] { - new Tuple("ModuleName","Required. Specifies the module name."), - new Tuple("GUID","Optional. Specifies the GUID of the module.") + var moduleKeys = new Tuple[2] + { + new Tuple("ModuleName", "Required. Specifies the module name."), + new Tuple("GUID", "Optional. Specifies the GUID of the module.") }; - //The following keys cannot be used together so they get their own table for lookup. - var moduleVersionKeys = new Dictionary(3, StringComparer.OrdinalIgnoreCase) { - {"ModuleVersion", "Specifies a minimum acceptable version of the module." }, - {"RequiredVersion", "Specifies an exact, required version of the module." }, - {"MaximumVersion", "Specifies the maximum acceptable version of the module." }, + + // The following keys cannot be used together so they get their own table for lookup. + var moduleVersionKeys = new Dictionary(3, StringComparer.OrdinalIgnoreCase) + { + { "ModuleVersion", "Specifies a minimum acceptable version of the module." }, + { "RequiredVersion", "Specifies an exact, required version of the module." }, + { "MaximumVersion", "Specifies the maximum acceptable version of the module." }, }; foreach (var value in moduleKeys) { @@ -5070,6 +5078,7 @@ internal static List CompleteComment(CompletionContext context } } } + return results; } From edc6ce193f5c35e760acea6b0ac0c731c96b9dc0 Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Fri, 15 Jan 2021 04:21:18 +0100 Subject: [PATCH 04/11] Add tests and use verbatim strings for regex. --- .../engine/CommandCompletion/CompletionCompleters.cs | 6 +++--- .../Host/TabCompletion/TabCompletion.Tests.ps1 | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 8edf5e8b5c3..ea0a5079af6 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4947,7 +4947,7 @@ internal static List CompleteComment(CompletionContext context } // Regex to find parameter like " -Parameter1" or " -" - MatchCollection foundMatches = Regex.Matches(lineToCursor, "\\s-([A-Za-z]+|$)"); + MatchCollection foundMatches = Regex.Matches(lineToCursor, @"\s-([A-Za-z]+|$)"); if (foundMatches.Count == 0) { return results; @@ -4980,7 +4980,7 @@ internal static List CompleteComment(CompletionContext context else { // Regex to find parameter values (any text that appears after various delimiters) - foundMatches = Regex.Matches(lineToCursor, "(\\s|,|;|{|\"|'|=)(\\w+|$)"); + foundMatches = Regex.Matches(lineToCursor, @"(\s|,|;|{|\""|'|=)(\w+|$)"); string currentValue; if (foundMatches.Count == 0) { @@ -5021,7 +5021,7 @@ internal static List CompleteComment(CompletionContext context string hashtableString = lineToCursor.Substring(hashtableStart); // Regex to find hashtable keys with or without quotes - foundMatches = Regex.Matches(hashtableString, "(@{|;)\\s*(?:'|\"|\\w*)\\w*"); + foundMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); var hashtableKeys = new string[foundMatches.Count]; for (int i = 0; i < hashtableKeys.Length; i++) { diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index c42f30e510a..906662acc57 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -908,6 +908,18 @@ Describe "TabCompletion" -Tags CI { $res.CompletionMatches[0].CompletionText | Should -BeExactly "Test history completion" } + It "Test requires parameter completion" { + $res = TabExpansion2 -inputScript "#requires -" -cursorColumn 11 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should -BeExactly "Modules" + } + + It "Test requires parameter value completion" { + $res = TabExpansion2 -inputScript "#requires -PSEdition " -cursorColumn 21 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should -BeExactly "Core" + } + It "Test Attribute member completion" { $inputStr = "function bar { [parameter(]param() }" $res = TabExpansion2 -inputScript $inputStr -cursorColumn ($inputStr.IndexOf('(') + 1) From a8761ca46021c7dfc4bd9343b6739e5970e2ac04 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 12 Apr 2021 11:31:09 -0700 Subject: [PATCH 05/11] Break into new method, use static readonly dictionaries --- .../CommandCompletion/CompletionCompleters.cs | 239 +++++++++--------- 1 file changed, 124 insertions(+), 115 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index ea0a5079af6..31263915a38 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4923,165 +4923,174 @@ private static SortedSet BuildSpecialVariablesCache() #region Comments - internal static List CompleteComment(CompletionContext context, ref int replacementIndex, ref int replacementLength) + private static List CompleteRequires(CompletionContext context, ref int replacementIndex, ref int replacementLength) { - List results = new List(); + var results = new List(); - // Complete #requires statements - if (context.WordToComplete.StartsWith("#requires ", StringComparison.OrdinalIgnoreCase)) + int cursorIndex = context.CursorPosition.ColumnNumber - 1; + + // cursor is within the "#requires " statement. + if (cursorIndex < 10) { - int cursorIndex = context.CursorPosition.ColumnNumber - 1; + return results; + } - // cursor is within the "#requires " statement. - if (cursorIndex < 10) - { - return results; - } + string lineToCursor = context.CursorPosition.Line.Substring(0, cursorIndex); - string lineToCursor = context.CursorPosition.Line.Substring(0, cursorIndex); + // RunAsAdministrator must be the last parameter in a Requires statement so no completion if the cursor is after the parameter. + if (lineToCursor.Contains(" -RunAsAdministrator", StringComparison.OrdinalIgnoreCase)) + { + return results; + } - // RunAsAdministrator must be the last parameter in a Requires statement so no completion if the cursor is after the parameter. - if (lineToCursor.Contains(" -RunAsAdministrator", StringComparison.OrdinalIgnoreCase)) - { - return results; - } + // Regex to find parameter like " -Parameter1" or " -" + MatchCollection foundMatches = Regex.Matches(lineToCursor, @"\s-([A-Za-z]+|$)"); + if (foundMatches.Count == 0) + { + return results; + } - // Regex to find parameter like " -Parameter1" or " -" - MatchCollection foundMatches = Regex.Matches(lineToCursor, @"\s-([A-Za-z]+|$)"); - if (foundMatches.Count == 0) - { - return results; - } + string currentParameter = foundMatches[^1].Groups[1].Value; - string currentParameter = foundMatches[^1].Groups[1].Value; + // Complete the parameter if the cursor is at a parameter + if (lineToCursor.LastIndexOf($"-{currentParameter}") + currentParameter.Length + 1 == cursorIndex) + { + replacementIndex = context.CursorPosition.Offset - currentParameter.Length; + replacementLength = currentParameter.Length; - // Complete the parameter if the cursor is at a parameter - if (lineToCursor.LastIndexOf($"-{currentParameter}") + currentParameter.Length + 1 == cursorIndex) + foreach (KeyValuePair parameter in s_requiresParameters) { - replacementIndex = context.CursorPosition.Offset - currentParameter.Length; - replacementLength = currentParameter.Length; - - var requiresParameters = new Tuple[4] + if (context.CursorPosition.Line.Contains($" -{parameter.Key}", StringComparison.OrdinalIgnoreCase) == false && + parameter.Key.StartsWith(currentParameter, StringComparison.OrdinalIgnoreCase)) { - new Tuple("Modules", "Specifies PowerShell modules that the script requires."), - new Tuple("PSEdition", "Specifies a PowerShell edition that the script requires."), - new Tuple("RunAsAdministrator", "Specifies that PowerShell must be running as administrator on Windows."), - new Tuple("Version", "Specifies the minimum version of PowerShell that the script requires.") - }; - foreach (var parameter in requiresParameters) - { - if (context.CursorPosition.Line.Contains($" -{parameter.Item1}", StringComparison.OrdinalIgnoreCase) == false && - parameter.Item1.StartsWith(currentParameter, StringComparison.OrdinalIgnoreCase)) - { - results.Add(new CompletionResult(parameter.Item1, parameter.Item1, CompletionResultType.ParameterName, parameter.Item2)); - } + results.Add(new CompletionResult(parameter.Key, parameter.Key, CompletionResultType.ParameterName, parameter.Key)); } } + } + else + { + // Regex to find parameter values (any text that appears after various delimiters) + foundMatches = Regex.Matches(lineToCursor, @"(\s|,|;|{|\""|'|=)(\w+|$)"); + string currentValue; + if (foundMatches.Count == 0) + { + currentValue = string.Empty; + } else { - // Regex to find parameter values (any text that appears after various delimiters) - foundMatches = Regex.Matches(lineToCursor, @"(\s|,|;|{|\""|'|=)(\w+|$)"); - string currentValue; - if (foundMatches.Count == 0) - { - currentValue = string.Empty; - } - else - { - currentValue = foundMatches[^1].Groups[^1].Value; - } + currentValue = foundMatches[^1].Groups[^1].Value; + } - replacementIndex = context.CursorPosition.Offset - currentValue.Length; - replacementLength = currentValue.Length; + replacementIndex = context.CursorPosition.Offset - currentValue.Length; + replacementLength = currentValue.Length; - if (currentParameter.Equals("PSEdition", StringComparison.OrdinalIgnoreCase)) + if (currentParameter.Equals("PSEdition", StringComparison.OrdinalIgnoreCase)) + { + foreach (KeyValuePair psEditionEntry in s_requiresPSEditions) { - var psEditionValues = new Tuple[2] - { - new Tuple("Core", "Specifies that the script requires PowerShell Core to run."), - new Tuple("Desktop", "Specifies that the script requires Windows PowerShell to run.") - }; - foreach (var value in psEditionValues) + if (psEditionEntry.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase)) { - if (value.Item1.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase)) - { - results.Add(new CompletionResult(value.Item1, value.Item1, CompletionResultType.ParameterValue, value.Item2)); - } + results.Add(new CompletionResult(psEditionEntry.Key, psEditionEntry.Key, CompletionResultType.ParameterValue, psEditionEntry.Value)); } } + } + + if (currentParameter.Equals("Modules", StringComparison.OrdinalIgnoreCase)) + { + int hashtableStart = lineToCursor.LastIndexOf("@{"); + int hashtableEnd = lineToCursor.LastIndexOf('}'); - if (currentParameter.Equals("Modules", StringComparison.OrdinalIgnoreCase)) + // Cursor is inside a hashtable + if (hashtableStart != -1 && (hashtableEnd == -1 || hashtableEnd < hashtableStart)) { - int hashtableStart = lineToCursor.LastIndexOf("@{"); - int hashtableEnd = lineToCursor.LastIndexOf('}'); + string hashtableString = lineToCursor.Substring(hashtableStart); - // Cursor is inside a hashtable - if (hashtableStart != -1 && (hashtableEnd == -1 || hashtableEnd < hashtableStart)) + // Regex to find hashtable keys with or without quotes + foundMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); + var hashtableKeys = new string[foundMatches.Count]; + for (int i = 0; i < hashtableKeys.Length; i++) { - string hashtableString = lineToCursor.Substring(hashtableStart); - - // Regex to find hashtable keys with or without quotes - foundMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); - var hashtableKeys = new string[foundMatches.Count]; - for (int i = 0; i < hashtableKeys.Length; i++) - { - hashtableKeys[i] = foundMatches[i].Value.TrimStart('@', '{', ';', '"', '\''); - } + hashtableKeys[i] = foundMatches[i].Value.TrimStart('@', '{', ';', '"', '\''); + } - var lastCaptureGroup = foundMatches[^1].Groups[0]; + var lastCaptureGroup = foundMatches[^1].Groups[0]; - // Are we completing a key for the hashtable? - if (lastCaptureGroup.Index + lastCaptureGroup.Length == hashtableString.Length) + // Are we completing a key for the hashtable? + if (lastCaptureGroup.Index + lastCaptureGroup.Length == hashtableString.Length) + { + foreach (KeyValuePair modSpecKey in s_requiresModuleSpecSharedKeys) { - var moduleKeys = new Tuple[2] - { - new Tuple("ModuleName", "Required. Specifies the module name."), - new Tuple("GUID", "Optional. Specifies the GUID of the module.") - }; - - // The following keys cannot be used together so they get their own table for lookup. - var moduleVersionKeys = new Dictionary(3, StringComparer.OrdinalIgnoreCase) - { - { "ModuleVersion", "Specifies a minimum acceptable version of the module." }, - { "RequiredVersion", "Specifies an exact, required version of the module." }, - { "MaximumVersion", "Specifies the maximum acceptable version of the module." }, - }; - foreach (var value in moduleKeys) + if (modSpecKey.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) && hashtableKeys.Contains(modSpecKey.Key) == false) { - if (value.Item1.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) && hashtableKeys.Contains(value.Item1) == false) - { - results.Add(new CompletionResult(value.Item1, value.Item1, CompletionResultType.ParameterValue, value.Item2)); - } - } - - foreach (string value in moduleVersionKeys.Keys) - { - if (value.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) && moduleVersionKeys.Keys.Intersect(hashtableKeys).Any() == false) - { - results.Add(new CompletionResult(value, value, CompletionResultType.ParameterValue, moduleVersionKeys[value])); - } + results.Add(new CompletionResult(modSpecKey.Key, modSpecKey.Key, CompletionResultType.ParameterValue, modSpecKey.Value)); } } - else + + foreach (string value in s_requiresModuleSpecExclusiveKeys.Keys) { - if (hashtableKeys[^1].Equals("ModuleName", StringComparison.OrdinalIgnoreCase)) + if (value.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) && s_requiresModuleSpecExclusiveKeys.Keys.Intersect(hashtableKeys).Any() == false) { - context.WordToComplete = currentValue; - return CompleteModuleName(context, true); + results.Add(new CompletionResult(value, value, CompletionResultType.ParameterValue, s_requiresModuleSpecExclusiveKeys[value])); } } } else { - context.WordToComplete = currentValue; - return CompleteModuleName(context, true); + if (hashtableKeys[^1].Equals("ModuleName", StringComparison.OrdinalIgnoreCase)) + { + context.WordToComplete = currentValue; + return CompleteModuleName(context, true); + } } } + else + { + context.WordToComplete = currentValue; + return CompleteModuleName(context, true); + } } + } - return results; + return results; + } + + private static readonly IReadOnlyDictionary s_requiresParameters = new SortedList(StringComparer.OrdinalIgnoreCase) + { + { "Modules", "Specifies PowerShell modules that the script requires." }, + { "PSEdition", "Specifies a PowerShell edition that the script requires." }, + { "RunAsAdministrator", "Specifies that PowerShell must be running as administrator on Windows." }, + { "Version", "Specifies the minimum version of PowerShell that the script requires." }, + }; + + private static readonly IReadOnlyDictionary s_requiresPSEditions = new SortedList(StringComparer.OrdinalIgnoreCase) + { + { "Core", "Specifies that the script requires PowerShell Core to run." }, + { "Desktop", "Specifies that the script requires Windows PowerShell to run." }, + }; + + private static readonly IReadOnlyDictionary s_requiresModuleSpecSharedKeys = new SortedList(StringComparer.OrdinalIgnoreCase) + { + { "ModuleName", "Required. Specifies the module name." }, + { "GUID", "Optional. Specifies the GUID of the module." }, + }; + + private static readonly IReadOnlyDictionary s_requiresModuleSpecExclusiveKeys = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "ModuleVersion", "Specifies a minimum acceptable version of the module." }, + { "RequiredVersion", "Specifies an exact, required version of the module." }, + { "MaximumVersion", "Specifies the maximum acceptable version of the module." }, + }; + + internal static List CompleteComment(CompletionContext context, ref int replacementIndex, ref int replacementLength) + { + // Complete #requires statements + if (context.WordToComplete.StartsWith("#requires ", StringComparison.OrdinalIgnoreCase)) + { + return CompleteRequires(context, ref replacementIndex, ref replacementLength); } + var results = new List(); + // Complete the history entries Match matchResult = Regex.Match(context.WordToComplete, @"^#([\w\-]*)$"); if (!matchResult.Success) { return results; } From 9f6905e486144cc5d8207f83a65fb0ce8df11085 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 12 Apr 2021 11:49:13 -0700 Subject: [PATCH 06/11] Make parameter completion more efficient --- .../CommandCompletion/CompletionCompleters.cs | 130 +++++++++--------- 1 file changed, 67 insertions(+), 63 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 31263915a38..f20c6ecfd29 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4944,111 +4944,115 @@ private static List CompleteRequires(CompletionContext context } // Regex to find parameter like " -Parameter1" or " -" - MatchCollection foundMatches = Regex.Matches(lineToCursor, @"\s-([A-Za-z]+|$)"); - if (foundMatches.Count == 0) + MatchCollection parameterMatches = Regex.Matches(lineToCursor, @"\s+-([A-Za-z]+|$)"); + if (parameterMatches.Count == 0) { return results; } - string currentParameter = foundMatches[^1].Groups[1].Value; + Group currentParameterMatch = parameterMatches[^1].Groups[1]; // Complete the parameter if the cursor is at a parameter - if (lineToCursor.LastIndexOf($"-{currentParameter}") + currentParameter.Length + 1 == cursorIndex) + if (currentParameterMatch.Index + currentParameterMatch.Length == cursorIndex) { - replacementIndex = context.CursorPosition.Offset - currentParameter.Length; - replacementLength = currentParameter.Length; + string currentParameterPrefix = currentParameterMatch.Value; + replacementIndex = context.CursorPosition.Offset - currentParameterPrefix.Length; + replacementLength = currentParameterPrefix.Length; + + // Produce completions for all parameters that begin with the prefix we've found, + // but which haven't already been specified in the line we need to complete foreach (KeyValuePair parameter in s_requiresParameters) { - if (context.CursorPosition.Line.Contains($" -{parameter.Key}", StringComparison.OrdinalIgnoreCase) == false && - parameter.Key.StartsWith(currentParameter, StringComparison.OrdinalIgnoreCase)) + if (parameter.Key.StartsWith(currentParameterPrefix, StringComparison.OrdinalIgnoreCase) + && !context.CursorPosition.Line.Contains($" -{parameter.Key}", StringComparison.OrdinalIgnoreCase)) { results.Add(new CompletionResult(parameter.Key, parameter.Key, CompletionResultType.ParameterName, parameter.Key)); } } + + return results; + } + + // Regex to find parameter values (any text that appears after various delimiters) + parameterMatches = Regex.Matches(lineToCursor, @"(\s+|,|;|{|\""|'|=)(\w+|$)"); + string currentValue; + if (parameterMatches.Count == 0) + { + currentValue = string.Empty; } else { - // Regex to find parameter values (any text that appears after various delimiters) - foundMatches = Regex.Matches(lineToCursor, @"(\s|,|;|{|\""|'|=)(\w+|$)"); - string currentValue; - if (foundMatches.Count == 0) - { - currentValue = string.Empty; - } - else - { - currentValue = foundMatches[^1].Groups[^1].Value; - } + currentValue = parameterMatches[^1].Groups[^1].Value; + } - replacementIndex = context.CursorPosition.Offset - currentValue.Length; - replacementLength = currentValue.Length; + replacementIndex = context.CursorPosition.Offset - currentValue.Length; + replacementLength = currentValue.Length; - if (currentParameter.Equals("PSEdition", StringComparison.OrdinalIgnoreCase)) + if (currentParameterMatch.Value.Equals("PSEdition", StringComparison.OrdinalIgnoreCase)) + { + foreach (KeyValuePair psEditionEntry in s_requiresPSEditions) { - foreach (KeyValuePair psEditionEntry in s_requiresPSEditions) + if (psEditionEntry.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase)) { - if (psEditionEntry.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase)) - { - results.Add(new CompletionResult(psEditionEntry.Key, psEditionEntry.Key, CompletionResultType.ParameterValue, psEditionEntry.Value)); - } + results.Add(new CompletionResult(psEditionEntry.Key, psEditionEntry.Key, CompletionResultType.ParameterValue, psEditionEntry.Value)); } } + } + + if (currentParameterMatch.Value.Equals("Modules", StringComparison.OrdinalIgnoreCase)) + { + int hashtableStart = lineToCursor.LastIndexOf("@{"); + int hashtableEnd = lineToCursor.LastIndexOf('}'); - if (currentParameter.Equals("Modules", StringComparison.OrdinalIgnoreCase)) + // Cursor is inside a hashtable + if (hashtableStart != -1 && (hashtableEnd == -1 || hashtableEnd < hashtableStart)) { - int hashtableStart = lineToCursor.LastIndexOf("@{"); - int hashtableEnd = lineToCursor.LastIndexOf('}'); + string hashtableString = lineToCursor.Substring(hashtableStart); - // Cursor is inside a hashtable - if (hashtableStart != -1 && (hashtableEnd == -1 || hashtableEnd < hashtableStart)) + // Regex to find hashtable keys with or without quotes + parameterMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); + var hashtableKeys = new string[parameterMatches.Count]; + for (int i = 0; i < hashtableKeys.Length; i++) { - string hashtableString = lineToCursor.Substring(hashtableStart); - - // Regex to find hashtable keys with or without quotes - foundMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); - var hashtableKeys = new string[foundMatches.Count]; - for (int i = 0; i < hashtableKeys.Length; i++) - { - hashtableKeys[i] = foundMatches[i].Value.TrimStart('@', '{', ';', '"', '\''); - } + hashtableKeys[i] = parameterMatches[i].Value.TrimStart('@', '{', ';', '"', '\''); + } - var lastCaptureGroup = foundMatches[^1].Groups[0]; + var lastCaptureGroup = parameterMatches[^1].Groups[0]; - // Are we completing a key for the hashtable? - if (lastCaptureGroup.Index + lastCaptureGroup.Length == hashtableString.Length) + // Are we completing a key for the hashtable? + if (lastCaptureGroup.Index + lastCaptureGroup.Length == hashtableString.Length) + { + foreach (KeyValuePair modSpecKey in s_requiresModuleSpecSharedKeys) { - foreach (KeyValuePair modSpecKey in s_requiresModuleSpecSharedKeys) - { - if (modSpecKey.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) && hashtableKeys.Contains(modSpecKey.Key) == false) - { - results.Add(new CompletionResult(modSpecKey.Key, modSpecKey.Key, CompletionResultType.ParameterValue, modSpecKey.Value)); - } - } - - foreach (string value in s_requiresModuleSpecExclusiveKeys.Keys) + if (modSpecKey.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) && hashtableKeys.Contains(modSpecKey.Key) == false) { - if (value.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) && s_requiresModuleSpecExclusiveKeys.Keys.Intersect(hashtableKeys).Any() == false) - { - results.Add(new CompletionResult(value, value, CompletionResultType.ParameterValue, s_requiresModuleSpecExclusiveKeys[value])); - } + results.Add(new CompletionResult(modSpecKey.Key, modSpecKey.Key, CompletionResultType.ParameterValue, modSpecKey.Value)); } } - else + + foreach (string value in s_requiresModuleSpecExclusiveKeys.Keys) { - if (hashtableKeys[^1].Equals("ModuleName", StringComparison.OrdinalIgnoreCase)) + if (value.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) && s_requiresModuleSpecExclusiveKeys.Keys.Intersect(hashtableKeys).Any() == false) { - context.WordToComplete = currentValue; - return CompleteModuleName(context, true); + results.Add(new CompletionResult(value, value, CompletionResultType.ParameterValue, s_requiresModuleSpecExclusiveKeys[value])); } } } else { - context.WordToComplete = currentValue; - return CompleteModuleName(context, true); + if (hashtableKeys[^1].Equals("ModuleName", StringComparison.OrdinalIgnoreCase)) + { + context.WordToComplete = currentValue; + return CompleteModuleName(context, true); + } } } + else + { + context.WordToComplete = currentValue; + return CompleteModuleName(context, true); + } } return results; From 898911733eb91a83d97a13cb530f558f19e57b32 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 12 Apr 2021 12:05:02 -0700 Subject: [PATCH 07/11] Tweak parameter value style --- .../CommandCompletion/CompletionCompleters.cs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index f20c6ecfd29..dc2f0b9bcf4 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4944,13 +4944,13 @@ private static List CompleteRequires(CompletionContext context } // Regex to find parameter like " -Parameter1" or " -" - MatchCollection parameterMatches = Regex.Matches(lineToCursor, @"\s+-([A-Za-z]+|$)"); - if (parameterMatches.Count == 0) + MatchCollection parameterValueMatches = Regex.Matches(lineToCursor, @"\s+-([A-Za-z]+|$)"); + if (parameterValueMatches.Count == 0) { return results; } - Group currentParameterMatch = parameterMatches[^1].Groups[1]; + Group currentParameterMatch = parameterValueMatches[^1].Groups[1]; // Complete the parameter if the cursor is at a parameter if (currentParameterMatch.Index + currentParameterMatch.Length == cursorIndex) @@ -4975,20 +4975,21 @@ private static List CompleteRequires(CompletionContext context } // Regex to find parameter values (any text that appears after various delimiters) - parameterMatches = Regex.Matches(lineToCursor, @"(\s+|,|;|{|\""|'|=)(\w+|$)"); + parameterValueMatches = Regex.Matches(lineToCursor, @"(\s+|,|;|{|\""|'|=)(\w+|$)"); string currentValue; - if (parameterMatches.Count == 0) + if (parameterValueMatches.Count == 0) { currentValue = string.Empty; } else { - currentValue = parameterMatches[^1].Groups[^1].Value; + currentValue = parameterValueMatches[^1].Groups[1].Value; } replacementIndex = context.CursorPosition.Offset - currentValue.Length; replacementLength = currentValue.Length; + // Complete PSEdition parameter values if (currentParameterMatch.Value.Equals("PSEdition", StringComparison.OrdinalIgnoreCase)) { foreach (KeyValuePair psEditionEntry in s_requiresPSEditions) @@ -4998,6 +4999,8 @@ private static List CompleteRequires(CompletionContext context results.Add(new CompletionResult(psEditionEntry.Key, psEditionEntry.Key, CompletionResultType.ParameterValue, psEditionEntry.Value)); } } + + return results; } if (currentParameterMatch.Value.Equals("Modules", StringComparison.OrdinalIgnoreCase)) @@ -5011,14 +5014,14 @@ private static List CompleteRequires(CompletionContext context string hashtableString = lineToCursor.Substring(hashtableStart); // Regex to find hashtable keys with or without quotes - parameterMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); - var hashtableKeys = new string[parameterMatches.Count]; + parameterValueMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); + var hashtableKeys = new string[parameterValueMatches.Count]; for (int i = 0; i < hashtableKeys.Length; i++) { - hashtableKeys[i] = parameterMatches[i].Value.TrimStart('@', '{', ';', '"', '\''); + hashtableKeys[i] = parameterValueMatches[i].Value.TrimStart('@', '{', ';', '"', '\''); } - var lastCaptureGroup = parameterMatches[^1].Groups[0]; + var lastCaptureGroup = parameterValueMatches[^1].Groups[0]; // Are we completing a key for the hashtable? if (lastCaptureGroup.Index + lastCaptureGroup.Length == hashtableString.Length) From 9d7a4a054be66e955f9ef4f7f981deeffa172515 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 12 Apr 2021 12:23:01 -0700 Subject: [PATCH 08/11] Style changes to hashtable key completion --- .../CommandCompletion/CompletionCompleters.cs | 87 +++++++++++-------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index dc2f0b9bcf4..d65e5114ea9 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -5003,58 +5003,64 @@ private static List CompleteRequires(CompletionContext context return results; } + // Complete Modules module specification values if (currentParameterMatch.Value.Equals("Modules", StringComparison.OrdinalIgnoreCase)) { int hashtableStart = lineToCursor.LastIndexOf("@{"); int hashtableEnd = lineToCursor.LastIndexOf('}'); - // Cursor is inside a hashtable - if (hashtableStart != -1 && (hashtableEnd == -1 || hashtableEnd < hashtableStart)) + bool insideHashtable = hashtableStart != -1 && (hashtableEnd == -1 || hashtableEnd < hashtableStart); + + // If not inside a hashtable, try to complete a module simple name + if (!insideHashtable) { - string hashtableString = lineToCursor.Substring(hashtableStart); + context.WordToComplete = currentValue; + return CompleteModuleName(context, true); + } + + string hashtableString = lineToCursor.Substring(hashtableStart); + + // Regex to find hashtable keys with or without quotes + parameterValueMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); + var hashtableKeys = new string[parameterValueMatches.Count]; + for (int i = 0; i < hashtableKeys.Length; i++) + { + hashtableKeys[i] = parameterValueMatches[i].Value.TrimStart(s_hashtableKeyPrefixes); + } + + Group lastHashtableKeyPrefixGroup = parameterValueMatches[^1].Groups[0]; - // Regex to find hashtable keys with or without quotes - parameterValueMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); - var hashtableKeys = new string[parameterValueMatches.Count]; - for (int i = 0; i < hashtableKeys.Length; i++) + // If we're not completing a key for the hashtable, try to complete module names, but nothing else + bool completingHashtableKey = lastHashtableKeyPrefixGroup.Index + lastHashtableKeyPrefixGroup.Length == hashtableString.Length; + if (!completingHashtableKey) + { + if (hashtableKeys[^1].Equals("ModuleName", StringComparison.OrdinalIgnoreCase)) { - hashtableKeys[i] = parameterValueMatches[i].Value.TrimStart('@', '{', ';', '"', '\''); + context.WordToComplete = currentValue; + return CompleteModuleName(context, true); } - var lastCaptureGroup = parameterValueMatches[^1].Groups[0]; + return results; + } - // Are we completing a key for the hashtable? - if (lastCaptureGroup.Index + lastCaptureGroup.Length == hashtableString.Length) - { - foreach (KeyValuePair modSpecKey in s_requiresModuleSpecSharedKeys) - { - if (modSpecKey.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) && hashtableKeys.Contains(modSpecKey.Key) == false) - { - results.Add(new CompletionResult(modSpecKey.Key, modSpecKey.Key, CompletionResultType.ParameterValue, modSpecKey.Value)); - } - } + // Now try to complete hashtable keys - foreach (string value in s_requiresModuleSpecExclusiveKeys.Keys) - { - if (value.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) && s_requiresModuleSpecExclusiveKeys.Keys.Intersect(hashtableKeys).Any() == false) - { - results.Add(new CompletionResult(value, value, CompletionResultType.ParameterValue, s_requiresModuleSpecExclusiveKeys[value])); - } - } - } - else + foreach (KeyValuePair modSpecKey in s_requiresModuleSpecSharedKeys) + { + if (modSpecKey.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) + && !hashtableKeys.Contains(modSpecKey.Key)) { - if (hashtableKeys[^1].Equals("ModuleName", StringComparison.OrdinalIgnoreCase)) - { - context.WordToComplete = currentValue; - return CompleteModuleName(context, true); - } + results.Add(new CompletionResult(modSpecKey.Key, modSpecKey.Key, CompletionResultType.ParameterValue, modSpecKey.Value)); } } - else + + foreach (string value in s_requiresModuleSpecExclusiveKeys.Keys) { - context.WordToComplete = currentValue; - return CompleteModuleName(context, true); + if (value.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) + && s_requiresModuleSpecExclusiveKeys.Keys.Intersect(hashtableKeys).Any()) + { + results.Add(new CompletionResult(value, value, CompletionResultType.ParameterValue, s_requiresModuleSpecExclusiveKeys[value])); + } } } @@ -5088,6 +5094,15 @@ private static List CompleteRequires(CompletionContext context { "MaximumVersion", "Specifies the maximum acceptable version of the module." }, }; + private static readonly char[] s_hashtableKeyPrefixes = new[] + { + '@', + '{', + ';', + '"', + '\'', + }; + internal static List CompleteComment(CompletionContext context, ref int replacementIndex, ref int replacementLength) { // Complete #requires statements From 923056a968fc451c821d039402589ba3383fa84a Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 12 Apr 2021 12:32:57 -0700 Subject: [PATCH 09/11] Improve module spec key suggestions --- .../CommandCompletion/CompletionCompleters.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index d65e5114ea9..08cdfb8520c 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -5045,6 +5045,7 @@ private static List CompleteRequires(CompletionContext context // Now try to complete hashtable keys + // First add any common keys that match the current one that haven't already been used foreach (KeyValuePair modSpecKey in s_requiresModuleSpecSharedKeys) { if (modSpecKey.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) @@ -5054,12 +5055,21 @@ private static List CompleteRequires(CompletionContext context } } - foreach (string value in s_requiresModuleSpecExclusiveKeys.Keys) + // Then, if the mutually-exclusive module version keys haven't been used, suggest those + bool alreadyHasExclusiveKeys = s_requiresModuleSpecExclusiveKeys.Keys.Intersect(hashtableKeys).Any(); + if (!alreadyHasExclusiveKeys) { - if (value.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) - && s_requiresModuleSpecExclusiveKeys.Keys.Intersect(hashtableKeys).Any()) + foreach (KeyValuePair exclusiveKeyEntry in s_requiresModuleSpecExclusiveKeys) { - results.Add(new CompletionResult(value, value, CompletionResultType.ParameterValue, s_requiresModuleSpecExclusiveKeys[value])); + if (exclusiveKeyEntry.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase)) + { + results.Add( + new CompletionResult( + exclusiveKeyEntry.Key, + exclusiveKeyEntry.Key, + CompletionResultType.ParameterValue, + exclusiveKeyEntry.Value)); + } } } } From 6e82e1ef0fb31283d968308ba61dc60eee2bdeb0 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 12 Apr 2021 13:14:16 -0700 Subject: [PATCH 10/11] Move methods and fix logic --- .../CommandCompletion/CompletionCompleters.cs | 237 +++++++++--------- 1 file changed, 123 insertions(+), 114 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 08cdfb8520c..670c8ada032 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4923,6 +4923,79 @@ private static SortedSet BuildSpecialVariablesCache() #region Comments + internal static List CompleteComment(CompletionContext context, ref int replacementIndex, ref int replacementLength) + { + // Complete #requires statements + if (context.WordToComplete.StartsWith("#requires ", StringComparison.OrdinalIgnoreCase)) + { + return CompleteRequires(context, ref replacementIndex, ref replacementLength); + } + + var results = new List(); + + // Complete the history entries + Match matchResult = Regex.Match(context.WordToComplete, @"^#([\w\-]*)$"); + if (!matchResult.Success) { return results; } + + string wordToComplete = matchResult.Groups[1].Value; + Collection psobjs; + + int entryId; + if (Regex.IsMatch(wordToComplete, @"^[0-9]+$") && LanguagePrimitives.TryConvertTo(wordToComplete, out entryId)) + { + context.Helper.AddCommandWithPreferenceSetting("Get-History", typeof(GetHistoryCommand)).AddParameter("Id", entryId); + psobjs = context.Helper.ExecuteCurrentPowerShell(out _); + + if (psobjs != null && psobjs.Count == 1) + { + var historyInfo = PSObject.Base(psobjs[0]) as HistoryInfo; + if (historyInfo != null) + { + var commandLine = historyInfo.CommandLine; + if (!string.IsNullOrEmpty(commandLine)) + { + // var tooltip = "Id: " + historyInfo.Id + "\n" + + // "ExecutionStatus: " + historyInfo.ExecutionStatus + "\n" + + // "StartExecutionTime: " + historyInfo.StartExecutionTime + "\n" + + // "EndExecutionTime: " + historyInfo.EndExecutionTime + "\n"; + // Use the commandLine as the Tooltip in case the commandLine is multiple lines of scripts + results.Add(new CompletionResult(commandLine, commandLine, CompletionResultType.History, commandLine)); + } + } + } + + return results; + } + + wordToComplete = "*" + wordToComplete + "*"; + context.Helper.AddCommandWithPreferenceSetting("Get-History", typeof(GetHistoryCommand)); + + psobjs = context.Helper.ExecuteCurrentPowerShell(out _); + var pattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase); + + if (psobjs != null) + { + for (int index = psobjs.Count - 1; index >= 0; index--) + { + var psobj = psobjs[index]; + if (!(PSObject.Base(psobj) is HistoryInfo historyInfo)) continue; + + var commandLine = historyInfo.CommandLine; + if (!string.IsNullOrEmpty(commandLine) && pattern.IsMatch(commandLine)) + { + // var tooltip = "Id: " + historyInfo.Id + "\n" + + // "ExecutionStatus: " + historyInfo.ExecutionStatus + "\n" + + // "StartExecutionTime: " + historyInfo.StartExecutionTime + "\n" + + // "EndExecutionTime: " + historyInfo.EndExecutionTime + "\n"; + // Use the commandLine as the Tooltip in case the commandLine is multiple lines of scripts + results.Add(new CompletionResult(commandLine, commandLine, CompletionResultType.History, commandLine)); + } + } + } + + return results; + } + private static List CompleteRequires(CompletionContext context, ref int replacementIndex, ref int replacementLength) { var results = new List(); @@ -4944,13 +5017,13 @@ private static List CompleteRequires(CompletionContext context } // Regex to find parameter like " -Parameter1" or " -" - MatchCollection parameterValueMatches = Regex.Matches(lineToCursor, @"\s+-([A-Za-z]+|$)"); - if (parameterValueMatches.Count == 0) + MatchCollection hashtableKeyMatches = Regex.Matches(lineToCursor, @"\s+-([A-Za-z]+|$)"); + if (hashtableKeyMatches.Count == 0) { return results; } - Group currentParameterMatch = parameterValueMatches[^1].Groups[1]; + Group currentParameterMatch = hashtableKeyMatches[^1].Groups[1]; // Complete the parameter if the cursor is at a parameter if (currentParameterMatch.Index + currentParameterMatch.Length == cursorIndex) @@ -4975,15 +5048,15 @@ private static List CompleteRequires(CompletionContext context } // Regex to find parameter values (any text that appears after various delimiters) - parameterValueMatches = Regex.Matches(lineToCursor, @"(\s+|,|;|{|\""|'|=)(\w+|$)"); + hashtableKeyMatches = Regex.Matches(lineToCursor, @"(\s+|,|;|{|\""|'|=)(\w+|$)"); string currentValue; - if (parameterValueMatches.Count == 0) + if (hashtableKeyMatches.Count == 0) { currentValue = string.Empty; } else { - currentValue = parameterValueMatches[^1].Groups[1].Value; + currentValue = hashtableKeyMatches[^1].Groups[2].Value; } replacementIndex = context.CursorPosition.Offset - currentValue.Length; @@ -5021,20 +5094,53 @@ private static List CompleteRequires(CompletionContext context string hashtableString = lineToCursor.Substring(hashtableStart); // Regex to find hashtable keys with or without quotes - parameterValueMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); - var hashtableKeys = new string[parameterValueMatches.Count]; - for (int i = 0; i < hashtableKeys.Length; i++) + hashtableKeyMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*"); + + // Build the list of keys we might want to complete, based on what's already been provided + var moduleSpecKeysToComplete = new HashSet(s_requiresModuleSpecKeys.Keys); + bool sawModuleNameLast = false; + foreach (Match existingHashtableKeyMatch in hashtableKeyMatches) { - hashtableKeys[i] = parameterValueMatches[i].Value.TrimStart(s_hashtableKeyPrefixes); + string existingHashtableKey = existingHashtableKeyMatch.Value.TrimStart(s_hashtableKeyPrefixes); + + if (string.IsNullOrEmpty(existingHashtableKey)) + { + continue; + } + + // Remove the existing key we just saw + moduleSpecKeysToComplete.Remove(existingHashtableKey); + + // We need to remember later if we saw "ModuleName" as the last hashtable key, for completions + if (sawModuleNameLast = existingHashtableKey.Equals("ModuleName", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // "RequiredVersion" is mutually exclusive with "ModuleVersion" and "MaximumVersion" + + if (existingHashtableKey.Equals("ModuleVersion", StringComparison.OrdinalIgnoreCase) + || existingHashtableKey.Equals("MaximumVersion", StringComparison.OrdinalIgnoreCase)) + { + moduleSpecKeysToComplete.Remove("RequiredVersion"); + continue; + } + + if (existingHashtableKey.Equals("RequiredVersion", StringComparison.OrdinalIgnoreCase)) + { + moduleSpecKeysToComplete.Remove("ModuleVersion"); + moduleSpecKeysToComplete.Remove("MaximumVersion"); + continue; + } } - Group lastHashtableKeyPrefixGroup = parameterValueMatches[^1].Groups[0]; + Group lastHashtableKeyPrefixGroup = hashtableKeyMatches[^1].Groups[0]; // If we're not completing a key for the hashtable, try to complete module names, but nothing else bool completingHashtableKey = lastHashtableKeyPrefixGroup.Index + lastHashtableKeyPrefixGroup.Length == hashtableString.Length; if (!completingHashtableKey) { - if (hashtableKeys[^1].Equals("ModuleName", StringComparison.OrdinalIgnoreCase)) + if (sawModuleNameLast) { context.WordToComplete = currentValue; return CompleteModuleName(context, true); @@ -5044,32 +5150,11 @@ private static List CompleteRequires(CompletionContext context } // Now try to complete hashtable keys - - // First add any common keys that match the current one that haven't already been used - foreach (KeyValuePair modSpecKey in s_requiresModuleSpecSharedKeys) + foreach (string moduleSpecKey in moduleSpecKeysToComplete) { - if (modSpecKey.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase) - && !hashtableKeys.Contains(modSpecKey.Key)) + if (moduleSpecKey.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase)) { - results.Add(new CompletionResult(modSpecKey.Key, modSpecKey.Key, CompletionResultType.ParameterValue, modSpecKey.Value)); - } - } - - // Then, if the mutually-exclusive module version keys haven't been used, suggest those - bool alreadyHasExclusiveKeys = s_requiresModuleSpecExclusiveKeys.Keys.Intersect(hashtableKeys).Any(); - if (!alreadyHasExclusiveKeys) - { - foreach (KeyValuePair exclusiveKeyEntry in s_requiresModuleSpecExclusiveKeys) - { - if (exclusiveKeyEntry.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase)) - { - results.Add( - new CompletionResult( - exclusiveKeyEntry.Key, - exclusiveKeyEntry.Key, - CompletionResultType.ParameterValue, - exclusiveKeyEntry.Value)); - } + results.Add(new CompletionResult(moduleSpecKey, moduleSpecKey, CompletionResultType.ParameterValue, s_requiresModuleSpecKeys[moduleSpecKey])); } } } @@ -5091,14 +5176,10 @@ private static List CompleteRequires(CompletionContext context { "Desktop", "Specifies that the script requires Windows PowerShell to run." }, }; - private static readonly IReadOnlyDictionary s_requiresModuleSpecSharedKeys = new SortedList(StringComparer.OrdinalIgnoreCase) + private static readonly IReadOnlyDictionary s_requiresModuleSpecKeys = new SortedList(StringComparer.OrdinalIgnoreCase) { { "ModuleName", "Required. Specifies the module name." }, { "GUID", "Optional. Specifies the GUID of the module." }, - }; - - private static readonly IReadOnlyDictionary s_requiresModuleSpecExclusiveKeys = new Dictionary(StringComparer.OrdinalIgnoreCase) - { { "ModuleVersion", "Specifies a minimum acceptable version of the module." }, { "RequiredVersion", "Specifies an exact, required version of the module." }, { "MaximumVersion", "Specifies the maximum acceptable version of the module." }, @@ -5111,81 +5192,9 @@ private static List CompleteRequires(CompletionContext context ';', '"', '\'', + ' ', }; - internal static List CompleteComment(CompletionContext context, ref int replacementIndex, ref int replacementLength) - { - // Complete #requires statements - if (context.WordToComplete.StartsWith("#requires ", StringComparison.OrdinalIgnoreCase)) - { - return CompleteRequires(context, ref replacementIndex, ref replacementLength); - } - - var results = new List(); - - // Complete the history entries - Match matchResult = Regex.Match(context.WordToComplete, @"^#([\w\-]*)$"); - if (!matchResult.Success) { return results; } - - string wordToComplete = matchResult.Groups[1].Value; - Collection psobjs; - - int entryId; - if (Regex.IsMatch(wordToComplete, @"^[0-9]+$") && LanguagePrimitives.TryConvertTo(wordToComplete, out entryId)) - { - context.Helper.AddCommandWithPreferenceSetting("Get-History", typeof(GetHistoryCommand)).AddParameter("Id", entryId); - psobjs = context.Helper.ExecuteCurrentPowerShell(out _); - - if (psobjs != null && psobjs.Count == 1) - { - var historyInfo = PSObject.Base(psobjs[0]) as HistoryInfo; - if (historyInfo != null) - { - var commandLine = historyInfo.CommandLine; - if (!string.IsNullOrEmpty(commandLine)) - { - // var tooltip = "Id: " + historyInfo.Id + "\n" + - // "ExecutionStatus: " + historyInfo.ExecutionStatus + "\n" + - // "StartExecutionTime: " + historyInfo.StartExecutionTime + "\n" + - // "EndExecutionTime: " + historyInfo.EndExecutionTime + "\n"; - // Use the commandLine as the Tooltip in case the commandLine is multiple lines of scripts - results.Add(new CompletionResult(commandLine, commandLine, CompletionResultType.History, commandLine)); - } - } - } - - return results; - } - - wordToComplete = "*" + wordToComplete + "*"; - context.Helper.AddCommandWithPreferenceSetting("Get-History", typeof(GetHistoryCommand)); - - psobjs = context.Helper.ExecuteCurrentPowerShell(out _); - var pattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase); - - if (psobjs != null) - { - for (int index = psobjs.Count - 1; index >= 0; index--) - { - var psobj = psobjs[index]; - if (!(PSObject.Base(psobj) is HistoryInfo historyInfo)) continue; - - var commandLine = historyInfo.CommandLine; - if (!string.IsNullOrEmpty(commandLine) && pattern.IsMatch(commandLine)) - { - // var tooltip = "Id: " + historyInfo.Id + "\n" + - // "ExecutionStatus: " + historyInfo.ExecutionStatus + "\n" + - // "StartExecutionTime: " + historyInfo.StartExecutionTime + "\n" + - // "EndExecutionTime: " + historyInfo.EndExecutionTime + "\n"; - // Use the commandLine as the Tooltip in case the commandLine is multiple lines of scripts - results.Add(new CompletionResult(commandLine, commandLine, CompletionResultType.History, commandLine)); - } - } - } - - return results; - } - #endregion Comments #region Members From 12ddfd6f5b68182f484ac04c49e8c2890dbd4b0c Mon Sep 17 00:00:00 2001 From: MartinGC94 Date: Wed, 14 Apr 2021 01:04:04 +0200 Subject: [PATCH 11/11] Add tests and fix minor issues. --- .../CommandCompletion/CompletionCompleters.cs | 15 ++--- .../TabCompletion/TabCompletion.Tests.ps1 | 61 ++++++++++++++++++- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 670c8ada032..1735528423d 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -4935,7 +4935,10 @@ internal static List CompleteComment(CompletionContext context // Complete the history entries Match matchResult = Regex.Match(context.WordToComplete, @"^#([\w\-]*)$"); - if (!matchResult.Success) { return results; } + if (!matchResult.Success) + { + return results; + } string wordToComplete = matchResult.Groups[1].Value; Collection psobjs; @@ -5001,13 +5004,6 @@ private static List CompleteRequires(CompletionContext context var results = new List(); int cursorIndex = context.CursorPosition.ColumnNumber - 1; - - // cursor is within the "#requires " statement. - if (cursorIndex < 10) - { - return results; - } - string lineToCursor = context.CursorPosition.Line.Substring(0, cursorIndex); // RunAsAdministrator must be the last parameter in a Requires statement so no completion if the cursor is after the parameter. @@ -5040,7 +5036,7 @@ private static List CompleteRequires(CompletionContext context if (parameter.Key.StartsWith(currentParameterPrefix, StringComparison.OrdinalIgnoreCase) && !context.CursorPosition.Line.Contains($" -{parameter.Key}", StringComparison.OrdinalIgnoreCase)) { - results.Add(new CompletionResult(parameter.Key, parameter.Key, CompletionResultType.ParameterName, parameter.Key)); + results.Add(new CompletionResult(parameter.Key, parameter.Key, CompletionResultType.ParameterName, parameter.Value)); } } @@ -5118,7 +5114,6 @@ private static List CompleteRequires(CompletionContext context } // "RequiredVersion" is mutually exclusive with "ModuleVersion" and "MaximumVersion" - if (existingHashtableKey.Equals("ModuleVersion", StringComparison.OrdinalIgnoreCase) || existingHashtableKey.Equals("MaximumVersion", StringComparison.OrdinalIgnoreCase)) { diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 906662acc57..6503ff8d7ec 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -908,18 +908,75 @@ Describe "TabCompletion" -Tags CI { $res.CompletionMatches[0].CompletionText | Should -BeExactly "Test history completion" } - It "Test requires parameter completion" { + It "Test #requires parameter completion" { $res = TabExpansion2 -inputScript "#requires -" -cursorColumn 11 $res.CompletionMatches.Count | Should -BeGreaterThan 0 $res.CompletionMatches[0].CompletionText | Should -BeExactly "Modules" } - It "Test requires parameter value completion" { + It "Test #requires parameter value completion" { $res = TabExpansion2 -inputScript "#requires -PSEdition " -cursorColumn 21 $res.CompletionMatches.Count | Should -BeGreaterThan 0 $res.CompletionMatches[0].CompletionText | Should -BeExactly "Core" } + It "Test no completion after #requires -RunAsAdministrator" { + $res = TabExpansion2 -inputScript "#requires -RunAsAdministrator -" -cursorColumn 31 + $res.CompletionMatches | Should -HaveCount 0 + } + + It "Test no suggestions for already existing parameters in #requires" { + $res = TabExpansion2 -inputScript "#requires -Modules -" -cursorColumn 20 + $res.CompletionMatches.CompletionText | Should -Not -Contain "Modules" + } + + It "Test module completion in #requires without quotes" { + $res = TabExpansion2 -inputScript "#requires -Modules P" -cursorColumn 20 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches.CompletionText | Should -Contain "Pester" + } + + It "Test module completion in #requires with quotes" { + $res = TabExpansion2 -inputScript '#requires -Modules "' -cursorColumn 20 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches.CompletionText | Should -Contain "Pester" + } + + It "Test module completion in #requires with multiple modules" { + $res = TabExpansion2 -inputScript "#requires -Modules Pester," -cursorColumn 26 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches.CompletionText | Should -Contain "Pester" + } + + It "Test hashtable key completion in #requires statement for modules" { + $res = TabExpansion2 -inputScript "#requires -Modules @{" -cursorColumn 21 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches[0].CompletionText | Should -BeExactly "GUID" + } + + It "Test no suggestions for already existing hashtable keys in #requires statement for modules" { + $res = TabExpansion2 -inputScript '#requires -Modules @{ModuleName="Pester";' -cursorColumn 41 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches.CompletionText | Should -Not -Contain "ModuleName" + } + + It "Test no suggestions for mutually exclusive hashtable keys in #requires statement for modules" { + $res = TabExpansion2 -inputScript '#requires -Modules @{ModuleName="Pester";RequiredVersion="1.0";' -cursorColumn 63 + $res.CompletionMatches.CompletionText | Should -BeExactly "GUID" + } + + It "Test no suggestions for RequiredVersion key in #requires statement when ModuleVersion is specified" { + $res = TabExpansion2 -inputScript '#requires -Modules @{ModuleName="Pester";ModuleVersion="1.0";' -cursorColumn 61 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches.CompletionText | Should -Not -Contain "RequiredVersion" + } + + It "Test module completion in #requires statement for hashtables" { + $res = TabExpansion2 -inputScript '#requires -Modules @{ModuleName="p' -cursorColumn 34 + $res.CompletionMatches.Count | Should -BeGreaterThan 0 + $res.CompletionMatches.CompletionText | Should -Contain "Pester" + } + It "Test Attribute member completion" { $inputStr = "function bar { [parameter(]param() }" $res = TabExpansion2 -inputScript $inputStr -cursorColumn ($inputStr.IndexOf('(') + 1)