Skip to content

Commit 0cbcf7b

Browse files
authored
Enable completion of variables across ScriptBlock scopes (#19819)
1 parent b9b5136 commit 0cbcf7b

File tree

2 files changed

+53
-5
lines changed

2 files changed

+53
-5
lines changed

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

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5375,6 +5375,20 @@ private static void AddUniqueVariable(HashSet<string> hashedResults, List<Comple
53755375
"pv"
53765376
};
53775377

5378+
private static readonly HashSet<string> s_localScopeCommandNames = new(StringComparer.OrdinalIgnoreCase)
5379+
{
5380+
"Microsoft.PowerShell.Core\\ForEach-Object",
5381+
"ForEach-Object",
5382+
"foreach",
5383+
"%",
5384+
"Microsoft.PowerShell.Core\\Where-Object",
5385+
"Where-Object",
5386+
"where",
5387+
"?",
5388+
"BeforeAll",
5389+
"BeforeEach"
5390+
};
5391+
53785392
private sealed class VariableInfo
53795393
{
53805394
internal Type LastDeclaredConstraint;
@@ -5616,12 +5630,31 @@ public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst fun
56165630

56175631
public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst)
56185632
{
5619-
return scriptBlockExpressionAst != Top ? AstVisitAction.SkipChildren : AstVisitAction.Continue;
5620-
}
5633+
if (scriptBlockExpressionAst == Top)
5634+
{
5635+
return AstVisitAction.Continue;
5636+
}
56215637

5622-
public override AstVisitAction VisitScriptBlock(ScriptBlockAst scriptBlockAst)
5623-
{
5624-
return scriptBlockAst != Top ? AstVisitAction.SkipChildren : AstVisitAction.Continue;
5638+
Ast parent = scriptBlockExpressionAst.Parent;
5639+
// This loop checks if the scriptblock is used as a command, or an argument for a command, eg: ForEach-Object -Process {$Var1 = "Hello"}, {Var2 = $true}
5640+
while (true)
5641+
{
5642+
if (parent is CommandAst cmdAst)
5643+
{
5644+
string cmdName = cmdAst.GetCommandName();
5645+
return s_localScopeCommandNames.Contains(cmdName)
5646+
|| (cmdAst.CommandElements[0] is ScriptBlockExpressionAst && cmdAst.InvocationOperator == TokenKind.Dot)
5647+
? AstVisitAction.Continue
5648+
: AstVisitAction.SkipChildren;
5649+
}
5650+
5651+
if (parent is not CommandExpressionAst and not PipelineAst and not StatementBlockAst and not ArrayExpressionAst and not ArrayLiteralAst)
5652+
{
5653+
return AstVisitAction.SkipChildren;
5654+
}
5655+
5656+
parent = parent.Parent;
5657+
}
56255658
}
56265659
}
56275660

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,21 @@ switch ($x)
274274
$completionText -join ' ' | Should -BeExactly 'Ascending Descending Expression'
275275
}
276276

277+
It 'Should complete variable assigned in other scriptblock' {
278+
$res = TabExpansion2 -inputScript 'ForEach-Object -Begin {$Test1 = "Hello"} -Process {$Test'
279+
$res.CompletionMatches[0].CompletionText | Should -Be '$Test1'
280+
}
281+
282+
It 'Should complete variable assigned in an array of scriptblocks' {
283+
$res = TabExpansion2 -inputScript 'ForEach-Object -Process @({"Block1"},{$Test1="Hello"});$Test'
284+
$res.CompletionMatches[0].CompletionText | Should -Be '$Test1'
285+
}
286+
287+
It 'Should not complete variable assigned in an ampersand executed scriptblock' {
288+
$res = TabExpansion2 -inputScript '& {$AmpeersandVarCompletionTest = "Hello"};$AmpeersandVarCompletionTes'
289+
$res.CompletionMatches.Count | Should -Be 0
290+
}
291+
277292
context TypeConstructionWithHashtable {
278293
BeforeAll {
279294
class RandomTestType {

0 commit comments

Comments
 (0)