mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Add completion for Requires statements (#14596)
This commit is contained in:
@@ -473,7 +473,7 @@ namespace System.Management.Automation
|
||||
break;
|
||||
|
||||
completionContext.WordToComplete = tokenAtCursor.Text;
|
||||
result = CompletionCompleters.CompleteComment(completionContext);
|
||||
result = CompletionCompleters.CompleteComment(completionContext, ref replacementIndex, ref replacementLength);
|
||||
break;
|
||||
|
||||
case TokenKind.StringExpandable:
|
||||
|
||||
@@ -4921,13 +4921,22 @@ namespace System.Management.Automation
|
||||
|
||||
#region Comments
|
||||
|
||||
// Complete the history entries
|
||||
internal static List<CompletionResult> CompleteComment(CompletionContext context)
|
||||
internal static List<CompletionResult> CompleteComment(CompletionContext context, ref int replacementIndex, ref int replacementLength)
|
||||
{
|
||||
List<CompletionResult> results = new List<CompletionResult>();
|
||||
// Complete #requires statements
|
||||
if (context.WordToComplete.StartsWith("#requires ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return CompleteRequires(context, ref replacementIndex, ref replacementLength);
|
||||
}
|
||||
|
||||
var results = new List<CompletionResult>();
|
||||
|
||||
// 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<PSObject> psobjs;
|
||||
@@ -4988,6 +4997,197 @@ namespace System.Management.Automation
|
||||
return results;
|
||||
}
|
||||
|
||||
private static List<CompletionResult> CompleteRequires(CompletionContext context, ref int replacementIndex, ref int replacementLength)
|
||||
{
|
||||
var results = new List<CompletionResult>();
|
||||
|
||||
int cursorIndex = context.CursorPosition.ColumnNumber - 1;
|
||||
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 hashtableKeyMatches = Regex.Matches(lineToCursor, @"\s+-([A-Za-z]+|$)");
|
||||
if (hashtableKeyMatches.Count == 0)
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
Group currentParameterMatch = hashtableKeyMatches[^1].Groups[1];
|
||||
|
||||
// Complete the parameter if the cursor is at a parameter
|
||||
if (currentParameterMatch.Index + currentParameterMatch.Length == cursorIndex)
|
||||
{
|
||||
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<string, string> parameter in s_requiresParameters)
|
||||
{
|
||||
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.Value));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Regex to find parameter values (any text that appears after various delimiters)
|
||||
hashtableKeyMatches = Regex.Matches(lineToCursor, @"(\s+|,|;|{|\""|'|=)(\w+|$)");
|
||||
string currentValue;
|
||||
if (hashtableKeyMatches.Count == 0)
|
||||
{
|
||||
currentValue = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentValue = hashtableKeyMatches[^1].Groups[2].Value;
|
||||
}
|
||||
|
||||
replacementIndex = context.CursorPosition.Offset - currentValue.Length;
|
||||
replacementLength = currentValue.Length;
|
||||
|
||||
// Complete PSEdition parameter values
|
||||
if (currentParameterMatch.Value.Equals("PSEdition", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
foreach (KeyValuePair<string, string> psEditionEntry in s_requiresPSEditions)
|
||||
{
|
||||
if (psEditionEntry.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
results.Add(new CompletionResult(psEditionEntry.Key, psEditionEntry.Key, CompletionResultType.ParameterValue, psEditionEntry.Value));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Complete Modules module specification values
|
||||
if (currentParameterMatch.Value.Equals("Modules", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
int hashtableStart = lineToCursor.LastIndexOf("@{");
|
||||
int hashtableEnd = lineToCursor.LastIndexOf('}');
|
||||
|
||||
bool insideHashtable = hashtableStart != -1 && (hashtableEnd == -1 || hashtableEnd < hashtableStart);
|
||||
|
||||
// If not inside a hashtable, try to complete a module simple name
|
||||
if (!insideHashtable)
|
||||
{
|
||||
context.WordToComplete = currentValue;
|
||||
return CompleteModuleName(context, true);
|
||||
}
|
||||
|
||||
string hashtableString = lineToCursor.Substring(hashtableStart);
|
||||
|
||||
// Regex to find hashtable keys with or without quotes
|
||||
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<string>(s_requiresModuleSpecKeys.Keys);
|
||||
bool sawModuleNameLast = false;
|
||||
foreach (Match existingHashtableKeyMatch in hashtableKeyMatches)
|
||||
{
|
||||
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 = 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 (sawModuleNameLast)
|
||||
{
|
||||
context.WordToComplete = currentValue;
|
||||
return CompleteModuleName(context, true);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Now try to complete hashtable keys
|
||||
foreach (string moduleSpecKey in moduleSpecKeysToComplete)
|
||||
{
|
||||
if (moduleSpecKey.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
results.Add(new CompletionResult(moduleSpecKey, moduleSpecKey, CompletionResultType.ParameterValue, s_requiresModuleSpecKeys[moduleSpecKey]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static readonly IReadOnlyDictionary<string, string> s_requiresParameters = new SortedList<string, string>(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<string, string> s_requiresPSEditions = new SortedList<string, string>(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<string, string> s_requiresModuleSpecKeys = new SortedList<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "ModuleName", "Required. Specifies the module name." },
|
||||
{ "GUID", "Optional. Specifies the GUID of the module." },
|
||||
{ "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." },
|
||||
};
|
||||
|
||||
private static readonly char[] s_hashtableKeyPrefixes = new[]
|
||||
{
|
||||
'@',
|
||||
'{',
|
||||
';',
|
||||
'"',
|
||||
'\'',
|
||||
' ',
|
||||
};
|
||||
|
||||
#endregion Comments
|
||||
|
||||
#region Members
|
||||
|
||||
@@ -908,6 +908,75 @@ 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 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)
|
||||
|
||||
Reference in New Issue
Block a user