diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs
index 081fbd07a7..7e3e52472c 100644
--- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs
+++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs
@@ -850,6 +850,16 @@ namespace System.Management.Automation
result = GetResultForEnumPropertyValueOfDSCResource(completionContext, string.Empty, ref replacementIndex, ref replacementLength, out unused);
break;
}
+ case TokenKind.Break:
+ case TokenKind.Continue:
+ {
+ if ((lastAst is BreakStatementAst breakStatement && breakStatement.Label is null)
+ || (lastAst is ContinueStatementAst continueStatement && continueStatement.Label is null))
+ {
+ result = CompleteLoopLabel(completionContext);
+ }
+ break;
+ }
default:
break;
}
@@ -1759,6 +1769,11 @@ namespace System.Management.Automation
var tokenAtCursorText = tokenAtCursor.Text;
completionContext.WordToComplete = tokenAtCursorText;
+ if (lastAst.Parent is BreakStatementAst || lastAst.Parent is ContinueStatementAst)
+ {
+ return CompleteLoopLabel(completionContext);
+ }
+
var strConst = lastAst as StringConstantExpressionAst;
if (strConst != null)
{
@@ -2213,5 +2228,40 @@ namespace System.Management.Automation
return result;
}
+
+ ///
+ /// Complete loop labels after labeled control flow statements such as Break and Continue.
+ ///
+ private static List CompleteLoopLabel(CompletionContext completionContext)
+ {
+ var result = new List();
+ foreach (Ast ast in completionContext.RelatedAsts)
+ {
+ if (ast is LabeledStatementAst labeledStatement
+ && labeledStatement.Label is not null
+ && (completionContext.WordToComplete is null || labeledStatement.Label.StartsWith(completionContext.WordToComplete, StringComparison.OrdinalIgnoreCase)))
+ {
+ result.Add(new CompletionResult(labeledStatement.Label, labeledStatement.Label, CompletionResultType.Text, labeledStatement.Extent.Text));
+ }
+ else if (ast is ErrorStatementAst errorStatement)
+ {
+ // Handles incomplete do/switch loops (other labeled statements do not need this special treatment)
+ // The regex looks for the loopLabel of errorstatements that look like do/switch loops
+ // For example in ":Label do " it will find "Label".
+ var labelMatch = Regex.Match(errorStatement.Extent.Text, @"(?<=^:)\w+(?=\s+(do|switch)\b(?!-))", RegexOptions.IgnoreCase);
+ if (labelMatch.Success)
+ {
+ result.Add(new CompletionResult(labelMatch.Value, labelMatch.Value, CompletionResultType.Text, errorStatement.Extent.Text));
+ }
+ }
+ }
+
+ if (result.Count == 0)
+ {
+ return null;
+ }
+
+ return result;
+ }
}
}
diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1
index b1e4b68a9b..d2fa38dd2b 100644
--- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1
+++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1
@@ -1179,6 +1179,63 @@ dir -Recurse `
$res.CompletionMatches | Should -HaveCount 2
[string]::Join(',', ($res.CompletionMatches.completiontext | Sort-Object)) | Should -BeExactly "1.0,1.1"
}
+ It '' -TestCases @(
+ @{
+ Intent = 'Complete loop labels with no input'
+ Expected = 'Outer','Inner'
+ TestString = ':Outer while ($true){:Inner while ($true){ break ^ }}'
+ }
+ @{
+ Intent = 'Complete loop labels that are accessible'
+ Expected = 'Outer'
+ TestString = ':Outer do {:Inner while ($true){ break } continue ^ } until ($false)'
+ }
+ @{
+ Intent = 'Complete loop labels with partial input'
+ Expected = 'Outer'
+ TestString = ':Outer do {:Inner while ($true){ break } continue o^ut } while ($true)'
+ }
+ @{
+ Intent = 'Complete loop label for incomplete switch'
+ Expected = 'Outer'
+ TestString = ':Outer switch ($x){"randomValue"{ continue ^'
+ }
+ @{
+ Intent = 'Complete loop label for incomplete do loop'
+ Expected = 'Outer'
+ TestString = ':Outer do {:Inner while ($true){ break } continue ^'
+ }
+ @{
+ Intent = 'Complete loop label for incomplete for loop'
+ Expected = 'forLoop'
+ TestString = ':forLoop for ($i = 0; $i -lt $SomeCollection.Count; $i++) {continue ^'
+ }
+ @{
+ Intent = 'Complete loop label for incomplete while loop'
+ Expected = 'WhileLoop'
+ TestString = ':WhileLoop while ($true){ break ^'
+ }
+ @{
+ Intent = 'Complete loop label for incomplete foreach loop'
+ Expected = 'foreachLoop'
+ TestString = ':foreachLoop foreach ($x in $y) { break ^'
+ }
+ @{
+ Intent = 'Not Complete loop labels with colon'
+ Expected = $null
+ TestString = ':Outer foreach ($x in $y){:Inner for ($i = 0; $i -lt $X.Count; $i++){ break :O^}}'
+ }
+ @{
+ Intent = 'Not Complete loop labels if cursor is in front of existing label'
+ Expected = $null
+ TestString = ':Outer switch ($x){"Value1"{break ^ Outer}}'
+ }
+ ){
+ param($Expected, $TestString)
+ $CursorIndex = $TestString.IndexOf('^')
+ $res = TabExpansion2 -cursorColumn $CursorIndex -inputScript $TestString.Remove($CursorIndex, 1)
+ $res.CompletionMatches.CompletionText | Should -BeExactly $Expected
+ }
}
Context "Module completion for 'using module'" {