8000 Add completion for Requires statements (#14596) · awakecoding/PowerShell@55d12ee · GitHub
[go: up one dir, main page]

Skip to content

Commit 55d12ee

Browse files
authored
Add completion for Requires statements (PowerShell#14596)
1 parent 4a7bea0 commit 55d12ee

File tree

3 files changed

+274
-5
lines changed

3 files changed

+274
-5
lines changed

src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ internal List<CompletionResult> GetResultHelper(CompletionContext completionCont
473473
break;
474474

475475
completionContext.WordToComplete = tokenAtCursor.Text;
476-
result = CompletionCompleters.CompleteComment(completionContext);
476+
result = CompletionCompleters.CompleteComment(completionContext, ref replacementIndex, ref replacementLength);
477477
break;
478478

479479
case TokenKind.StringExpandable:

src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs

Lines changed: 204 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4921,13 +4921,22 @@ private static SortedSet<string> BuildSpecialVariablesCache()
49214921

49224922
#region Comments
49234923

4924-
// Complete the history entries
4925-
internal static List<CompletionResult> CompleteComment(CompletionContext context)
4924+
internal static List<CompletionResult> CompleteComment(CompletionContext context, ref int replacementIndex, ref int replacementLength)
49264925
{
4927-
List<CompletionResult> results = new List<CompletionResult>();
4926+
// Complete #requires statements
4927+
if (context.WordToComplete.StartsWith("#requires ", StringComparison.OrdinalIgnoreCase))
4928+
{
4929+
return CompleteRequires(context, ref replacementIndex, ref replacementLength);
4930+
}
49284931

4932+
var results = new List<CompletionResult>();
4933+
4934+
// Complete the history entries
49294935
Match matchResult = Regex.Match(context.WordToComplete, @"^#([\w\-]*)$");
4930-
if (!matchResult.Success) { return results; }
4936+
if (!matchResult.Success)
4937+
{
4938+
return results;
4939+
}
49314940

49324941
string wordToComplete = matchResult.Groups[1].Value;
49334942
Collection<PSObject> psobjs;
@@ -4988,6 +4997,197 @@ internal static List<CompletionResult> CompleteComment(CompletionContext context
49884997
return results;
49894998
}
49904999

5000+
private static List<CompletionResult> CompleteRequires(CompletionContext context, ref int replacementIndex, ref int replacementLength)
5001+
{
5002+
var results = new List<CompletionResult>();
5003+
5004+
int cursorIndex = context.CursorPosition.ColumnNumber - 1;
5005+
string lineToCursor = context.CursorPosition.Line.Substring(0, cursorIndex);
5006+
5007+
// RunAsAdministrator must be the last parameter in a Requires statement so no completion if the cursor is after the parameter.
5008+
if (lineToCursor.Contains(" -RunAsAdministrator", StringComparison.OrdinalIgnoreCase))
5009+
{
5010+
return results;
5011+
}
5012+
5013+
// Regex to find parameter like " -Parameter1" or " -"
5014+
MatchCollection hashtableKeyMatches = Regex.Matches(lineToCursor, @"\s+-([A-Za-z]+|$)");
5015+
if (hashtableKeyMatches.Count == 0)
5016+
{
5017+
return results;
5018+
}
5019+
5020+
Group currentParameterMatch = hashtableKeyMatches[^1].Groups[1];
5021+
5022+
// Complete the parameter if the cursor is at a parameter
5023+
if (currentParameterMatch.Index + currentParameterMatch.Length == cursorIndex)
5024+
{
5025+
string currentParameterPrefix = currentParameterMatch.Value;
5026+
5027+
replacementIndex = context.CursorPosition.Offset - currentParameterPrefix.Length;
5028+
replacementLength = currentParameterPrefix.Length;
5029+
5030+
// Produce completions for all parameters that begin with the prefix we've found,
5031+
// but which haven't already been specified in the line we need to complete
5032+
foreach (KeyValuePair<string, string> parameter in s_requiresParameters)
5033+
{
5034+
if (parameter.Key.StartsWith(currentParameterPrefix, StringComparison.OrdinalIgnoreCase)
5035+
&& !context.CursorPosition.Line.Contains($" -{parameter.Key}", StringComparison.OrdinalIgnoreCase))
5036+
{
5037+
results.Add(new CompletionResult(parameter.Key, parameter.Key, CompletionResultType.ParameterName, parameter.Value));
5038+
}
5039+
}
5040+
5041+
return results;
5042+
}
5043+
5044+
// Regex to find parameter values (any text that appears after various delimiters)
5045+
hashtableKeyMatches = Regex.Matches(lineToCursor, @"(\s+|,|;|{|\""|'|=)(\w+|$)");
5046+
string currentValue;
5047+
if (hashtableKeyMatches.Count == 0)
5048+
{
5049+
currentValue = string.Empty;
5050+
}
5051+
else
5052+
{
5053+
currentValue = hashtableKeyMatches[^1].Groups[2].Value;
5054+
}
5055+
5056+
replacementIndex = context.CursorPosition.Offset - currentValue.Length;
5057+
replacementLength = currentValue.Length;
5058+
5059+
// Complete PSEdition parameter values
5060+
if (currentParameterMatch.Value.Equals("PSEdition", StringComparison.OrdinalIgnoreCase))
5061+
{
5062+
foreach (KeyValuePair<string, string> psEditionEntry in s_requiresPSEditions)
5063+
{
5064+
if (psEditionEntry.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase))
5065+
{
5066+
results.Add(new CompletionResult(psEditionEntry.Key, psEditionEntry.Key, CompletionResultType.ParameterValue, psEditionEntry.Value));
5067+
}
5068+
}
5069+
5070+
return results;
5071+
}
5072+
5073+
// Complete Modules module specification values
5074+
if (currentParameterMatch.Value.Equals("Modules", StringComparison.OrdinalIgnoreCase))
5075+
{
5076+
int hashtableStart = lineToCursor.LastIndexOf("@{");
5077+
int hashtableEnd = lineToCursor.LastIndexOf('}');
5078+
5079+
bool insideHashtable = hashtableStart != -1 && (hashtableEnd == -1 || hashtableEnd < hashtableStart);
5080+
5081+
// If not inside a hashtable, try to complete a module simple name
5082+
if (!insideHashtable)
5083+
{
5084+
context.WordToComplete = currentValue;
5085+
return CompleteModuleName(context, true);
5086+
}
5087+
5088+
string hashtableString = lineToCursor.Substring(hashtableStart);
5089+
5090+
// Regex to find hashtable keys with or without quotes
5091+
hashtableKeyMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*");
5092+
5093+
// Build the list of keys we might want to complete, based on what's already been provided
5094+
var moduleSpecKeysToComplete = new HashSet<string>(s_requiresModuleSpecKeys.Keys);
5095+
bool sawModuleNameLast = false;
5096+
foreach (Match existingHashtableKeyMatch in hashtableKeyMatches)
5097+
{
5098+
string existingHashtableKey = existingHashtableKeyMatch.Value.TrimStart(s_hashtableKeyPrefixes);
5099+
5100+
if (string.IsNullOrEmpty(existingHashtableKey))
5101+
{
5102+
continue;
5103+
}
5104+
5105+
// Remove the existing key we just saw
5106+
moduleSpecKeysToComplete.Remove(existingHashtableKey);
5107+
5108+
// We need to remember later if we saw "ModuleName" as the last hashtable key, for completions
5109+
if (sawModuleNameLast = existingHashtableKey.Equals("ModuleName", StringComparison.OrdinalIgnoreCase))
5110+
{
5111+
continue;
5112+
}
5113+
5114+
// "RequiredVersion" is mutually exclusive with "ModuleVersion" and "MaximumVersion"
5115+
if (existingHashtableKey.Equals("ModuleVersion", StringComparison.OrdinalIgnoreCase)
5116+
|| existingHashtableKey.Equals("MaximumVersion", StringComparison.OrdinalIgnoreCase))
5117+
{
5118+
moduleSpecKeysToComplete.Remove("RequiredVersion");
5119+
continue;
5120+
}
5121+
5122+
if (existingHashtableKey.Equals("RequiredVersion", StringComparison.OrdinalIgnoreCase))
5123+
{
5124+
moduleSpecKeysToComplete.Remove("ModuleVersion");
5125+
moduleSpecKeysToComplete.Remove("MaximumVersion");
5126+
continue;
5127+
}
5128+
}
5129+
5130+
Group lastHashtableKeyPrefixGroup = hashtableKeyMatches[^1].Groups[0];
5131+
5132+
// If we're not completing a key for the hashtable, try to complete module names, but nothing else
5133+
bool completingHashtableKey = lastHashtableKeyPrefixGroup.Index + lastHashtableKeyPrefixGroup.Length == hashtableString.Length;
5134+
if (!completingHashtableKey)
5135+
{
5136+
if (sawModuleNameLast)
5137+
{
5138+
context.WordToComplete = currentValue;
5139+
return CompleteModuleName(context, true);
5140+
}
5141+
5142+
return results;
5143+
}
5144+
5145+
// Now try to complete hashtable keys
5146+
foreach (string moduleSpecKey in moduleSpecKeysToComplete)
5147+
{
5148+
if (moduleSpecKey.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase))
5149+
{
5150+
results.Add(new CompletionResult(moduleSpecKey, moduleSpecKey, CompletionResultType.ParameterValue, s_requiresModuleSpecKeys[moduleSpecKey]));
5151+
}
5152+
}
5153+
}
5154+
5155+
return results;
5156+
}
5157+
5158+
private static readonly IReadOnlyDictionary<string, string> s_requiresParameters = new SortedList<string, string>(StringComparer.OrdinalIgnoreCase)
5159+
{
5160+
{ "Modules", "Specifies PowerShell modules that the script requires." },
5161+
{ "PSEdition", "Specifies a PowerShell edition that the script requires." },
5162+
{ "RunAsAdministrator", "Specifies that PowerShell must be running as administrator on Windows." },
5163+
{ "Version", "Specifies the minimum version of PowerShell that the script requires." },
5164+
};
5165+
5166+
private static readonly IReadOnlyDictionary<string, string> s_requiresPSEditions = new SortedList<string, string>(StringComparer.OrdinalIgnoreCase)
5167+
{
5168+
{ "Core", "Specifies that the script requires PowerShell Core to run." },
5169+
{ "Desktop", "Specifies that the script requires Windows PowerShell to run." },
5170+
};
5171+
5172+
private static readonly IReadOnlyDictionary<string, string> s_requiresModuleSpecKeys = new SortedList<string, string>(StringComparer.OrdinalIgnoreCase)
5173+
{
5174+
{ "ModuleName", "Required. Specifies the module name." },
5175+
{ "GUID", "Optional. Specifies the GUID of the module." },
5176+
{ "ModuleVersion", "Specifies a minimum acceptable version of the module." },
5177+
{ "RequiredVersion", "Specifies an exact, required version of the module." },
5178+
{ "MaximumVersion", "Specifies the maximum acceptable version of the module." },
5179+
};
5180+
5181+
private static readonly char[] s_hashtableKeyPrefixes = new[]
5182+
{
5183+
'@',
5184+
'{',
5185+
';',
5186+
'"',
5187+
'\'',
5188+
' ',
5189+
};
5190+
49915191
#endregion Comments
49925192

49935193
#region Members

test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,75 @@ Describe "TabCompletion" -Tags CI {
908908
$res.CompletionMatches[0].CompletionText | Should -BeExactly "Test history completion"
909909
}
910910

911+
It "Test #requires parameter completion" {
912+
$res = TabExpansion2 -inputScript "#requires -" -cursorColumn 11
913+
$res.CompletionMatches.Count | Should -BeGreaterThan 0
914+
$res.CompletionMatches[0].CompletionText | Should -BeExactly "Modules"
915+
}
916+
917+
It "Test #requires parameter value completion" {
918+
$res = TabExpansion2 -inputScript "#requires -PSEdition " -cursorColumn 21
919+
$res.CompletionMatches.Count | Should -BeGreaterThan 0
920+
$res.CompletionMatches[0].CompletionText | Should -BeExactly "Core"
921+
}
922+
923+
It "Test no completion after #requires -RunAsAdministrator" {
924+
$res = TabExpansion2 -inputScript "#requires -RunAsAdministrator -" -cursorColumn 31
925+
$res.CompletionMatches | Should -HaveCount 0
926+
}
927+
928+
It "Test no suggestions for already existing parameters in #requires" {
929+
$res = TabExpansion2 -inputScript "#requires -Modules -" -cursorColumn 20
930+
$res.CompletionMatches.CompletionText | Should -Not -Contain "Modules"
931+
}
932+
933+
It "Test module completion in #requires without quotes" {
934+
$res = TabExpansion2 -inputScript "#requires -Modules P" -cursorColumn 20
935+
$res.CompletionMatches.Count | Should -BeGreaterThan 0
936+
$res.CompletionMatches.CompletionText | Should -Contain "Pester"
937+
}
938+
939+
It "Test module completion in #requires with quotes" {
940+
$res = TabExpansion2 -inputScript '#requires -Modules "' -cursorColumn 20
941+
$res.CompletionMatches.Count | Should -BeGreaterThan 0
942+
$res.CompletionMatches.CompletionText | Should -Contain "Pester"
943+
}
944+
945+
It "Test module completion in #requires with multiple modules" {
946+
$res = TabExpansion2 -inputScript "#requires -Modules Pester," -cursorColumn 26
947+
$res.CompletionMatches.Count | Should -BeGreaterThan 0
948+
$res.CompletionMatches.CompletionText | Should -Contain "Pester"
949+
}
950+
951+
It "Test hashtable key completion in #requires statement for modules" {
952+
$res = TabExpansion2 -inputScript "#requires -Modules @{" -cursorColumn 21
953+
$res.CompletionMatches.Count | Should -BeGreaterThan 0
954+
$res.CompletionMatches[0].CompletionText | Should -BeExactly "GUID"
955+
}
956+
957+
It "Test no suggestions for already existing hashtable keys in #requires statement for modules" {
958+
$res = TabExpansion2 -inputScript '#requires -Modules @{ModuleName="Pester";' -cursorColumn 41
959+
$res.CompletionMatches.Count | Should -BeGreaterThan 0
960+
$res.CompletionMatches.CompletionText | Should -Not -Contain "ModuleName"
961+
}
962+
963+
It "Test no suggestions for mutually exclusive hashtable keys in #requires statement for modules" {
964+
$res = TabExpansion2 -inputScript '#requires -Modules @{ModuleName="Pester";RequiredVersion="1.0";' -cursorColumn 63
965+
$res.CompletionMatches.CompletionText | Should -BeExactly "GUID"
966+
}
967+
968+
It "Test no suggestions for RequiredVersion key in #requires statement when ModuleVersion is specified" {
969+
$res = TabExpansion2 -inputScript '#requires -Modules @{ModuleName="Pester";ModuleVersion="1.0";' -cursorColumn 61
970+
$res.CompletionMatches.Count | Should -BeGreaterThan 0
971+
$res.CompletionMatches.CompletionText | Should -Not -Contain "RequiredVersion"
972+
}
973+
974+
It "Test module completion in #requires statement for hashtables" {
975+
$res = TabExpansion2 -inputScript '#requires -Modules @{ModuleName="p' -cursorColumn 34
976+
$res.CompletionMatches.Count | Should -BeGreaterThan 0
977+
$res.CompletionMatches.CompletionText | Should -Contain "Pester"
978+
}
979+
911980
It "Test Attribute member completion" {
912981
$inputStr = "function bar { [parameter(]param() }"
913982
$res = TabExpansion2 -inputScript $inputStr -cursorColumn ($inputStr.IndexOf('(') + 1)

0 commit comments

Comments
 (0)
0