Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -290,68 +290,6 @@ private static bool CompleteOperator(Token tokenAtCursor, Ast lastAst)
return false;
}

private static bool CompleteAgainstStatementFlags(Ast scriptAst, Ast lastAst, Token token, out TokenKind kind)
{
kind = TokenKind.Unknown;

// Handle "switch -f<tab>"
var errorStatement = lastAst as ErrorStatementAst;
if (errorStatement != null && errorStatement.Kind != null)
{
switch (errorStatement.Kind.Kind)
{
case TokenKind.Switch:
kind = TokenKind.Switch;
return true;
default:
break;
}
}

// Handle "switch -<tab>". Skip cases like "switch ($a) {} -<tab> "
var scriptBlockAst = scriptAst as ScriptBlockAst;
if (token != null && token.Kind == TokenKind.Minus && scriptBlockAst != null)
{
var asts = AstSearcher.FindAll(scriptBlockAst, ast => IsCursorAfterExtent(token.Extent.StartScriptPosition, ast.Extent), searchNestedScriptBlocks: true);

Ast last = asts.LastOrDefault();
errorStatement = null;

while (last != null)
{
errorStatement = last as ErrorStatementAst;
if (errorStatement != null) { break; }

last = last.Parent;
}

if (errorStatement != null && errorStatement.Kind != null)
{
switch (errorStatement.Kind.Kind)
{
case TokenKind.Switch:

Tuple<Token, Ast> value;
if (errorStatement.Flags != null && errorStatement.Flags.TryGetValue(Parser.VERBATIM_ARGUMENT, out value))
{
if (IsTokenTheSame(value.Item1, token))
{
kind = TokenKind.Switch;
return true;
}
}

break;

default:
break;
}
}
}

return false;
}

private static bool IsTokenTheSame(Token x, Token y)
{
if (x.Kind == y.Kind && x.TokenFlags == y.TokenFlags &&
Expand Down Expand Up @@ -447,10 +385,9 @@ internal List<CompletionResult> GetResultHelper(CompletionContext completionCont
break;
}

TokenKind statementKind;
if (CompleteAgainstStatementFlags(null, lastAst, null, out statementKind))
result = CompletionCompleters.CompleteStatementFlags(completionContext);
if (result is not null)
{
result = CompletionCompleters.CompleteStatementFlags(statementKind, completionContext.WordToComplete);
break;
}

Expand Down Expand Up @@ -649,13 +586,7 @@ internal List<CompletionResult> GetResultHelper(CompletionContext completionCont
break;
}

// Handle the flag completion for statements, such as the switch statement
if (CompleteAgainstStatementFlags(completionContext.RelatedAsts[0], null, tokenAtCursor, out statementKind))
{
completionContext.WordToComplete = tokenAtCursor.Text;
result = CompletionCompleters.CompleteStatementFlags(statementKind, completionContext.WordToComplete);
break;
}
result = CompletionCompleters.CompleteStatementFlags(completionContext);

break;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7077,60 +7077,191 @@ internal static List<CompletionResult> CompleteHelpTopics(CompletionContext cont
#endregion Help Topics

#region Statement Parameters
private static readonly string[] s_SwitchParameters = new string[]
{
"CaseSensitive",
"Exact",
"File",
"Regex",
"Wildcard"
};

private static readonly HashSet<string> s_SwitchExclusiveParameters = new(StringComparer.OrdinalIgnoreCase)
{
"Exact",
"Regex",
"Wildcard"
};

internal static List<CompletionResult> CompleteStatementFlags(TokenKind kind, string wordToComplete)
internal static List<CompletionResult> CompleteStatementFlags(CompletionContext context)
{
switch (kind)
if (context.TokenAtCursor is null)
{
case TokenKind.Switch:
return null;
}

Diagnostics.Assert(!string.IsNullOrEmpty(wordToComplete) && wordToComplete[0].IsDash(), "the word to complete should start with '-'");
wordToComplete = wordToComplete.Substring(1);
bool withColon = wordToComplete.EndsWith(':');
wordToComplete = withColon ? wordToComplete.Remove(wordToComplete.Length - 1) : wordToComplete;
string parameterText = context.TokenAtCursor.Text.Substring(1);
var usedParameters = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
ErrorStatementAst incompleteStatement = null;
TokenKind statementKind = TokenKind.Unknown;

string enumString = LanguagePrimitives.EnumSingleTypeConverter.EnumValues(typeof(SwitchFlags));
string separator = CultureInfo.CurrentUICulture.TextInfo.ListSeparator;
string[] enumArray = enumString.Split(separator, StringSplitOptions.RemoveEmptyEntries);
switch (context.RelatedAsts[^1])
{
case DataStatementAst:
statementKind = TokenKind.Data;
break;

var pattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase);
var enumList = new List<string>();
var result = new List<CompletionResult>();
CompletionResult fullMatch = null;
case SwitchStatementAst switchStatement:
statementKind = TokenKind.Switch;

foreach (string value in enumArray)
if (switchStatement.Flags == SwitchFlags.None)
{
if (value.Equals(SwitchFlags.None.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; }
break;
}

if (wordToComplete.Equals(value, StringComparison.OrdinalIgnoreCase))
foreach (SwitchFlags item in Enum.GetValues(typeof(SwitchFlags)))
{
string itemAsString = item.ToString();
if ((switchStatement.Flags & item) != 0 && (parameterText.Equals(string.Empty) || !itemAsString.StartsWith(parameterText, StringComparison.OrdinalIgnoreCase)))
{
string completionText = withColon ? "-" + value + ":" : "-" + value;
fullMatch = new CompletionResult(completionText, value, CompletionResultType.ParameterName, value);
continue;
_ = usedParameters.Add(itemAsString);
}
}
break;

if (pattern.IsMatch(value))
case ErrorStatementAst errorStatement:
incompleteStatement = errorStatement;
break;

default:
Ast parent = context.RelatedAsts[^1].Parent;
while (parent is not null)
{
if (parent.Extent.StartOffset < context.TokenAtCursor.Extent.StartOffset && parent is NamedBlockAst namedBlock)
{
enumList.Add(value);
for (int i = namedBlock.Statements.Count - 1; i >= 0; i--)
{
if (namedBlock.Statements[i].Extent.StartOffset < context.TokenAtCursor.Extent.StartOffset
&& namedBlock.Statements[i] is ErrorStatementAst errStatement)
{
incompleteStatement = errStatement;
break;
}
}
if (incompleteStatement is null)
{
return null;
}
else
{
break;
}
}
parent = parent.Parent;
}
break;
}

if (fullMatch != null)
{
result.Add(fullMatch);
}
if (incompleteStatement is not null)
{
if (incompleteStatement.Kind is null)
{
return null;
}

enumList.Sort();
result.AddRange(from entry in enumList
let completionText = withColon ? "-" + entry + ":" : "-" + entry
select new CompletionResult(completionText, entry, CompletionResultType.ParameterName, entry));
switch (incompleteStatement.Kind.Kind)
{
case TokenKind.Data:
statementKind = TokenKind.Data;
// An ErrorStatement for Data can only have null or 1 flag set, if the cursor is not at that flag then there's no valid completions
if (incompleteStatement.Flags is null
|| incompleteStatement.Flags.Values.First().Item1.Extent.StartOffset != context.TokenAtCursor.Extent.StartOffset)
{
return null;
}
break;

return result;
case TokenKind.Switch:
statementKind = TokenKind.Switch;
if (incompleteStatement.Flags is null)
{
return null;
}

int highestStartOffset = 0;
foreach (var key in incompleteStatement.Flags.Keys)
{
var paramToken = incompleteStatement.Flags[key].Item1;
if (paramToken.Extent.StartOffset != context.TokenAtCursor.Extent.StartOffset)
{
_ = usedParameters.Add(key);
}

if (paramToken.Extent.StartOffset > highestStartOffset)
{
highestStartOffset = paramToken.Extent.StartOffset;
}
}

// The incomplete statement doesn't include parameters after the condition parentheses
// So if the tokenAtCursor is greater than the highest flag parameter then the user pressed tab like: switch () -<Tab>
if (highestStartOffset < context.TokenAtCursor.Extent.StartOffset)
{
return null;
}
break;

default:
return null;
}
}

var result = new List<CompletionResult>();

switch (statementKind)
{
case TokenKind.Data:
if ("SupportedCommand".StartsWith(parameterText, StringComparison.OrdinalIgnoreCase))
{
result.Add(new CompletionResult(
"-SupportedCommand",
"SupportedCommand",
CompletionResultType.ParameterName,
ResourceManagerCache.GetResourceString(
typeof(CompletionCompleters).Assembly,
"System.Management.Automation.resources.TabCompletionStrings",
"dataSupportedCommandParam")));
}
break;

case TokenKind.Switch:
var exclusiveParameterUsed = usedParameters.Overlaps(s_SwitchExclusiveParameters);
foreach (var parameter in s_SwitchParameters)
{
if (parameter.StartsWith(parameterText, StringComparison.OrdinalIgnoreCase)
&& !usedParameters.Contains(parameter)
&& !(exclusiveParameterUsed && s_SwitchExclusiveParameters.Contains(parameter)))
{
result.Add(new CompletionResult(
$"-{parameter}",
parameter,
CompletionResultType.ParameterName,
ResourceManagerCache.GetResourceString(
typeof(CompletionCompleters).Assembly,
"System.Management.Automation.resources.TabCompletionStrings",
$"switch{parameter}Param")));
}
}
break;

default:
break;
}


if (result.Count > 0)
{
return result;
}
return null;
}

Expand Down
9 changes: 8 additions & 1 deletion src/System.Management.Automation/engine/parser/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5726,7 +5726,14 @@ private StatementAst DataStatementRule(Token dataToken)

if (endErrorStatement != null)
{
return new ErrorStatementAst(ExtentOf(dataToken, endErrorStatement), GetNestedErrorAsts(commands));
Dictionary<string, Tuple<Token, Ast>> flags = null;
if ((supportedCommandToken is not null && supportedCommandToken.Kind == TokenKind.Minus)
|| supportedCommandToken is ParameterToken)
{
flags = new Dictionary<string, Tuple<Token, Ast>>(1, StringComparer.OrdinalIgnoreCase);
flags.Add(supportedCommandToken.Text, new Tuple<Token, Ast>(supportedCommandToken, null));
}
return new ErrorStatementAst(ExtentOf(dataToken, endErrorStatement), dataToken, flags, null, GetNestedErrorAsts(commands));
}

return new DataStatementAst(ExtentOf(dataToken, body), dataVariableName, commands, body);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,40 @@
<data name="shrOperatorDescription" xml:space="preserve">
<value>Shift Right bit operator. Inserts zero in the left-most bit position. For signed values, sign bit is preserved.</value>
</data>
<data name="switchCaseSensitiveParam" xml:space="preserve">
<value>[switch]
Performs a case-sensitive match.
If the match clause is not a string, this parameter is ignored.</value>
</data>
<data name="switchExactParam" xml:space="preserve">
<value>[switch]
(Default behavior) Indicates that the match clause, if it is a string, must match exactly.
If the match clause is not a string, this parameter is ignored.
Cannot be used with "Regex" and "Wildcard"</value>
</data>
<data name="switchFileParam" xml:space="preserve">
<value>[string]
Takes input from a file rather than an expression.
Each line of the file is read and evaluated by the switch statement.</value>
</data>
<data name="switchRegexParam" xml:space="preserve">
<value>[switch]
Performs regular expression matching of the value to the condition.
If the match clause is not a string, this parameter is ignored.
The $matches automatic variable is available for use within the matching statement block.
Cannot be used with "Exact" and "Wildcard"</value>
</data>
<data name="switchWildcardParam" xml:space="preserve">
<value>[switch]
Indicates that the condition is a wildcard string.
If the match clause is not a string, the parameter is ignored.
Cannot be used with "Exact" and "Regex"</value>
</data>
<data name="dataSupportedCommandParam" xml:space="preserve">
<value>[string[]]
Allows the specified commands to be used inside the data section.
Each command name should be separated by a comma and should not be quoted.</value>
</data>
<data name="AssemblyKeywordDescription" xml:space="preserve">
<value>Specifies the path to a .NET assembly to load.

Expand Down
Loading