Skip to content
Merged
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 @@ -850,6 +850,16 @@ internal List<CompletionResult> GetResultHelper(CompletionContext completionCont
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;
}
Expand Down Expand Up @@ -1759,6 +1769,11 @@ private List<CompletionResult> GetResultForIdentifier(CompletionContext completi
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)
{
Expand Down Expand Up @@ -2205,5 +2220,40 @@ private static List<CompletionResult> CompleteFileNameAsCommand(CompletionContex

return result;
}

/// <summary>
/// Complete loop labels after labeled control flow statements such as Break and Continue.
/// </summary>
private static List<CompletionResult> CompleteLoopLabel(CompletionContext completionContext)
{
var result = new List<CompletionResult>();
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;
}
}
}
57 changes: 57 additions & 0 deletions test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,63 @@ dir -Recurse `
$res.CompletionMatches | Should -HaveCount 2
[string]::Join(',', ($res.CompletionMatches.completiontext | Sort-Object)) | Should -BeExactly "1.0,1.1"
}
It '<Intent>' -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'" {
Expand Down