From bc39584e0f3c12644367aaff4ab89f8e198e04da Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 24 Apr 2017 01:49:43 +0200 Subject: [PATCH 1/7] Adding failing test --- .../TabCompletion/TabCompletion.Tests.ps1 | 97 ------------------- 1 file changed, 97 deletions(-) diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index a462b0a3099..e69de29bb2d 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -1,97 +0,0 @@ -Describe "TabCompletion" -Tags CI { - It 'Should complete Command' { - $res = TabExpansion2 -inputScript 'Get-Com' -cursorColumn 'Get-Com'.Length - $res.CompletionMatches[0].CompletionText | Should be Get-Command - } - - It 'Should complete native exe' -Skip:(!$IsWindows) { - $res = TabExpansion2 -inputScript 'notep' -cursorColumn 'notep'.Length - $res.CompletionMatches[0].CompletionText | Should be notepad.exe - } - - It 'Should complete dotnet method' { - $res = TabExpansion2 -inputScript '(1).ToSt' -cursorColumn '(1).ToSt'.Length - $res.CompletionMatches[0].CompletionText | Should be 'ToString(' - } - - It 'Should complete Magic foreach' { - $res = TabExpansion2 -inputScript '(1..10).Fo' -cursorColumn '(1..10).Fo'.Length - $res.CompletionMatches[0].CompletionText | Should be 'Foreach(' - } - - It 'Should complete types' { - $res = TabExpansion2 -inputScript '[pscu' -cursorColumn '[pscu'.Length - $res.CompletionMatches[0].CompletionText | Should be 'pscustomobject' - } - - It 'Should complete namespaces' { - $res = TabExpansion2 -inputScript 'using namespace Sys' -cursorColumn 'using namespace Sys'.Length - $res.CompletionMatches[0].CompletionText | Should be 'System' - } - - - It 'Should complete format-table hashtable' { - $res = TabExpansion2 -inputScript 'Get-ChildItem | Format-Table @{ ' -cursorColumn 'Get-ChildItem | Format-Table @{ '.Length - $res.CompletionMatches.Count | Should Be 5 - $res.CompletionMatches.Foreach{$_.CompletionText -in 'Label', 'Width', 'Alignment', 'Expression', 'FormatString' | Should Be $true} - } - - - It 'Should complete format-* hashtable on GroupBy' -TestCases ( - @{cmd = 'Format-Table'}, - @{cmd = 'Format-List'}, - @{cmd = 'Format-Wide'}, - @{cmd = 'Format-Custom'} - ) { - param($cmd) - $res = TabExpansion2 -inputScript "Get-ChildItem | $cmd -GroupBy @{ " -cursorColumn "Get-ChildItem | $cmd -GroupBy @{ ".Length - $res.CompletionMatches.Count | Should Be 3 - $res.CompletionMatches.Foreach{$_.CompletionText -in 'Label', 'Expression', 'FormatString' | Should Be $true} - } - - It 'Should complete format-list hashtable' { - $res = TabExpansion2 -inputScript 'Get-ChildItem | Format-List @{ ' -cursorColumn 'Get-ChildItem | Format-List @{ '.Length - $res.CompletionMatches.Count | Should Be 3 - $res.CompletionMatches.Foreach{$_.CompletionText -in 'Label', 'Expression', 'FormatString' | Should Be $true} - } - - It 'Should complete format-wide hashtable' { - $res = TabExpansion2 -inputScript 'Get-ChildItem | Format-Wide @{ ' -cursorColumn 'Get-ChildItem | Format-Wide @{ '.Length - $res.CompletionMatches.Count | Should Be 2 - $res.CompletionMatches.Foreach{$_.CompletionText -in 'Expression', 'FormatString' | Should Be $true} - } - - It 'Should complete format-custom hashtable' { - $res = TabExpansion2 -inputScript 'Get-ChildItem | Format-Custom @{ ' -cursorColumn 'Get-ChildItem | Format-Custom @{ '.Length - $res.CompletionMatches.Count | Should Be 2 - $res.CompletionMatches.Foreach{$_.CompletionText -in 'Expression', 'Depth' | Should Be $true} - } - - It 'Should complete Select-Object hashtable' { - $res = TabExpansion2 -inputScript 'Get-ChildItem | Select-Object @{ ' -cursorColumn 'Get-ChildItem | Select-Object @{ '.Length - $res.CompletionMatches.Count | Should Be 2 - $res.CompletionMatches.Foreach{$_.CompletionText -in 'Name', 'Expression' | Should Be $true} - } - - It 'Should complete Sort-Object hashtable' { - $res = TabExpansion2 -inputScript 'Get-ChildItem | Sort-Object @{ ' -cursorColumn 'Get-ChildItem | Sort-Object @{ '.Length - $res.CompletionMatches.Count | Should Be 3 - $res.CompletionMatches.Foreach{$_.CompletionText -in 'Expression', 'Ascending', 'Descending' | Should Be $true} - } - - It 'Should complete New-Object hashtable' { - class X { - $A - $B - $C - } - $res = TabExpansion2 -inputScript 'New-Object -TypeName X -Property @{ ' -cursorColumn 'New-Object -TypeName X -Property @{ '.Length - $res.CompletionMatches.Count | Should Be 3 - $res.CompletionMatches.Foreach{$_.CompletionText -in 'A', 'B', 'C' | Should Be $true} - } - - It 'Should complete keyword' -skip { - $res = TabExpansion2 -inputScript 'using nam' -cursorColumn 'using nam'.Length - $res.CompletionMatches[0].CompletionText | Should be 'namespace' - } -} From 361cf7c552cda5266ceddec031170be3a1f1f495 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Wed, 4 Jan 2017 01:23:46 +0100 Subject: [PATCH 2/7] Extract variable tokenAtCursorText from tokenAtCursor.Text. --- .../CommandCompletion/CompletionAnalysis.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 32057fce1c4..af06ef0aaa7 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -1347,7 +1347,8 @@ private List GetResultForIdentifier(CompletionContext completi var lastAst = completionContext.RelatedAsts.Last(); List result = null; - completionContext.WordToComplete = tokenAtCursor.Text; + var tokenAtCursorText = tokenAtCursor.Text; + completionContext.WordToComplete = tokenAtCursorText; var strConst = lastAst as StringConstantExpressionAst; if (strConst != null) @@ -1413,7 +1414,7 @@ private List GetResultForIdentifier(CompletionContext completi { Ast cursorAst = null; var cursorPosition = (InternalScriptPosition)_cursorPosition; - int offsetBeforeCmdName = cursorPosition.Offset - tokenAtCursor.Text.Length; + int offsetBeforeCmdName = cursorPosition.Offset - tokenAtCursorText.Length; if (offsetBeforeCmdName >= 0) { var cursorBeforeCmdName = cursorPosition.CloneWithNewOffset(offsetBeforeCmdName); @@ -1425,10 +1426,10 @@ private List GetResultForIdentifier(CompletionContext completi cursorAst.Extent.EndLineNumber == tokenAtCursor.Extent.StartLineNumber && cursorAst.Extent.EndColumnNumber == tokenAtCursor.Extent.StartColumnNumber) { - if (tokenAtCursor.Text.IndexOfAny(Utils.Separators.Directory) == 0) + if (tokenAtCursorText.IndexOfAny(Utils.Separators.Directory) == 0) { string wordToComplete = - CompletionCompleters.ConcatenateStringPathArguments(cursorAst as CommandElementAst, tokenAtCursor.Text, completionContext); + CompletionCompleters.ConcatenateStringPathArguments(cursorAst as CommandElementAst, tokenAtCursorText, completionContext); if (wordToComplete != null) { completionContext.WordToComplete = wordToComplete; @@ -1446,7 +1447,7 @@ private List GetResultForIdentifier(CompletionContext completi string fullPath = variableAst != null ? CompletionCompleters.CombineVariableWithPartialPath( variableAst: variableAst, - extraText: tokenAtCursor.Text, + extraText: tokenAtCursorText, executionContext: completionContext.ExecutionContext) : null; @@ -1532,7 +1533,7 @@ private List GetResultForIdentifier(CompletionContext completi return result; } - if (tokenAtCursor.Text.Length == 1 && tokenAtCursor.Text[0].IsDash() && (lastAst.Parent is CommandAst || lastAst.Parent is DynamicKeywordStatementAst)) + if (tokenAtCursorText.Length == 1 && tokenAtCursorText[0].IsDash() && (lastAst.Parent is CommandAst || lastAst.Parent is DynamicKeywordStatementAst)) { // When it's the content of a quoted string, we only handle variable/member completion if (isQuotedString) { return result; } @@ -1550,12 +1551,12 @@ private List GetResultForIdentifier(CompletionContext completi // We need to know if the previous element before the token is adjacent because // we don't have a MemberExpressionAst, we might have 2 command arguments. - if (tokenAtCursor.Text.Equals(TokenKind.Dot.Text(), StringComparison.Ordinal)) + if (tokenAtCursorText.Equals(TokenKind.Dot.Text(), StringComparison.Ordinal)) { memberOperator = TokenKind.Dot; isMemberCompletion = true; } - else if (tokenAtCursor.Text.Equals(TokenKind.ColonColon.Text(), StringComparison.Ordinal)) + else if (tokenAtCursorText.Equals(TokenKind.ColonColon.Text(), StringComparison.Ordinal)) { memberOperator = TokenKind.ColonColon; isMemberCompletion = true; @@ -1601,7 +1602,7 @@ private List GetResultForIdentifier(CompletionContext completi { if (!isWildcard && memberOperator != TokenKind.Unknown) { - replacementIndex += tokenAtCursor.Text.Length; + replacementIndex += tokenAtCursorText.Length; replacementLength = 0; } return result; @@ -1631,7 +1632,7 @@ private List GetResultForIdentifier(CompletionContext completi completionContext.WordToComplete = wordToComplete; } } - else if (tokenAtCursor.Text.IndexOfAny(Utils.Separators.Directory) == 0) + else if (tokenAtCursorText.IndexOfAny(Utils.Separators.Directory) == 0) { var command = lastAst.Parent as CommandBaseAst; if (command != null && command.Redirections.Any()) @@ -1642,7 +1643,7 @@ private List GetResultForIdentifier(CompletionContext completi fileRedirection.Extent.EndColumnNumber == lastAst.Extent.StartColumnNumber) { string wordToComplete = - CompletionCompleters.ConcatenateStringPathArguments(fileRedirection.Location, tokenAtCursor.Text, completionContext); + CompletionCompleters.ConcatenateStringPathArguments(fileRedirection.Location, tokenAtCursorText, completionContext); if (wordToComplete != null) { From 35cb4a15182d0044dac7bd530fb12c30de5ac2f0 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Wed, 4 Jan 2017 01:27:27 +0100 Subject: [PATCH 3/7] Adding support for double-dash native completion --- .../CommandCompletion/CompletionAnalysis.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index af06ef0aaa7..5c1f595fa1b 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -327,6 +327,7 @@ internal List GetResultHelper(CompletionContext completionCont case TokenKind.Multiply: case TokenKind.Generic: + case TokenKind.MinusMinus: // for native commands '--' case TokenKind.Identifier: result = GetResultForIdentifier(completionContext, ref replacementIndex, ref replacementLength, isQuotedString); break; @@ -1533,11 +1534,22 @@ private List GetResultForIdentifier(CompletionContext completi return result; } - if (tokenAtCursorText.Length == 1 && tokenAtCursorText[0].IsDash() && (lastAst.Parent is CommandAst || lastAst.Parent is DynamicKeywordStatementAst)) + var isSingleDash = tokenAtCursorText.Length == 1 && tokenAtCursorText[0].IsDash(); + var isDoubleDash = tokenAtCursorText.Length == 2 && tokenAtCursorText[0].IsDash() && tokenAtCursorText[1].IsDash(); + var isParentCommandOrDynamicKeyword = (lastAst.Parent is CommandAst || lastAst.Parent is DynamicKeywordStatementAst); + if ((isSingleDash || isDoubleDash) && isParentCommandOrDynamicKeyword) { // When it's the content of a quoted string, we only handle variable/member completion - if (isQuotedString) { return result; } - return CompletionCompleters.CompleteCommandParameter(completionContext); + if (isSingleDash) + { + if (isQuotedString) { return result; } + var res = CompletionCompleters.CompleteCommandParameter(completionContext); + if (res.Count != 0) + { + return res; + } + } + return CompletionCompleters.CompleteCommandArgument(completionContext); } TokenKind memberOperator = TokenKind.Unknown; From 4fbaaa7a64fdaf4ad9534fd346b23bd356649378 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 24 Apr 2017 00:57:39 +0200 Subject: [PATCH 4/7] Cleanup coaleasing null checks and type patterns. --- .../CommandCompletion/CompletionAnalysis.cs | 328 ++++++++---------- 1 file changed, 154 insertions(+), 174 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 5c1f595fa1b..bdaa209567b 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -114,7 +114,7 @@ internal CompletionContext CreateCompletionContext(ExecutionContext executionCon else { var stringExpandableToken = tokenAtCursor as StringExpandableToken; - if (stringExpandableToken != null && stringExpandableToken.NestedTokens != null) + if (stringExpandableToken?.NestedTokens != null) { tokenAtCursor = stringExpandableToken.NestedTokens.LastOrDefault( @@ -156,7 +156,7 @@ private static bool CompleteAgainstSwitchFile(Ast lastAst, Token tokenBeforeCurs Tuple fileConditionTuple; var errorStatement = lastAst as ErrorStatementAst; - if (errorStatement != null && errorStatement.Flags != null && errorStatement.Kind != null && tokenBeforeCursor != null && + if (errorStatement?.Flags != null && errorStatement.Kind != null && tokenBeforeCursor != null && errorStatement.Kind.Kind.Equals(TokenKind.Switch) && errorStatement.Flags.TryGetValue("file", out fileConditionTuple)) { // Handle "switch -file " @@ -173,7 +173,7 @@ private static bool CompleteAgainstSwitchFile(Ast lastAst, Token tokenBeforeCurs } errorStatement = pipeline.Parent as ErrorStatementAst; - if (errorStatement == null || errorStatement.Kind == null || errorStatement.Flags == null) + if (errorStatement?.Kind == null || errorStatement.Flags == null) { return false; } @@ -206,15 +206,13 @@ private static bool CompleteAgainstStatementFlags(Ast scriptAst, Ast lastAst, To // Handle "switch -f" var errorStatement = lastAst as ErrorStatementAst; - if (errorStatement != null && errorStatement.Kind != null) + if (errorStatement?.Kind != null) { switch (errorStatement.Kind.Kind) { case TokenKind.Switch: kind = TokenKind.Switch; return true; - default: - break; } } @@ -234,14 +232,12 @@ private static bool CompleteAgainstStatementFlags(Ast scriptAst, Ast lastAst, To last = last.Parent; } - if (errorStatement != null && errorStatement.Kind != null) + if (errorStatement?.Kind != null) { switch (errorStatement.Kind.Kind) { case TokenKind.Switch: - - Tuple value; - if (errorStatement.Flags != null && errorStatement.Flags.TryGetValue(Parser.VERBATIM_ARGUMENT, out value)) + if (errorStatement.Flags != null && errorStatement.Flags.TryGetValue(Parser.VERBATIM_ARGUMENT, out Tuple value)) { if (IsTokenTheSame(value.Item1, token)) { @@ -250,9 +246,6 @@ private static bool CompleteAgainstStatementFlags(Ast scriptAst, Ast lastAst, To } } break; - - default: - break; } } } @@ -395,9 +388,8 @@ internal List GetResultHelper(CompletionContext completionCont break; case TokenKind.RBracket: - if (lastAst is TypeExpressionAst) + if (lastAst is TypeExpressionAst targetExpr) { - var targetExpr = (TypeExpressionAst)lastAst; var memberResult = new List(); CompletionCompleters.CompleteMemberHelper( @@ -483,7 +475,6 @@ internal List GetResultHelper(CompletionContext completionCont { completionContext.WordToComplete = tokenAtCursor.Text; result = CompletionCompleters.CompleteStatementFlags(statementKind, completionContext.WordToComplete); - break; } break; @@ -493,10 +484,9 @@ internal List GetResultHelper(CompletionContext completionCont ConfigurationDefinitionAst configureAst = GetAncestorConfigurationAstAndKeywordAst( completionContext.CursorPosition, lastAst, out keywordAst); Diagnostics.Assert(configureAst != null, "ConfigurationDefinitionAst should never be null"); - bool matched = false; completionContext.WordToComplete = tokenAtCursor.Text.Trim(); // Current token is within ConfigurationDefinitionAst or DynamicKeywordStatementAst - return GetResultForIdentifierInConfiguration(completionContext, configureAst, null, out matched); + return GetResultForIdentifierInConfiguration(completionContext, configureAst, null, out _); } case TokenKind.Equals: case TokenKind.AtParen: @@ -691,8 +681,6 @@ internal List GetResultHelper(CompletionContext completionCont ref replacementIndex, ref replacementLength, out unused); } break; - default: - break; } } } @@ -883,7 +871,7 @@ internal static TypeName FindTypeNameToComplete(ITypeName type, IScriptPosition var arrayTypeName = type as ArrayTypeName; if (arrayTypeName != null) { - return FindTypeNameToComplete(arrayTypeName.ElementType, cursor) ?? null; + return FindTypeNameToComplete(arrayTypeName.ElementType, cursor); } return null; @@ -982,136 +970,134 @@ private List GetResultForEnumPropertyValueOfDSCResource( { IScriptPosition cursor = completionContext.CursorPosition; var keyValuePairWithCursor = GetHashEntryContainsCursor(cursor, hashTableAst, isCursorInString); - if (keyValuePairWithCursor != null) + var propertyNameAst = keyValuePairWithCursor?.Item1 as StringConstantExpressionAst; + if (propertyNameAst != null) { - var propertyNameAst = keyValuePairWithCursor.Item1 as StringConstantExpressionAst; - if (propertyNameAst != null) + DynamicKeywordProperty property; + if (keywordAst.Keyword.Properties.TryGetValue(propertyNameAst.Value, out property)) { - DynamicKeywordProperty property; - if (keywordAst.Keyword.Properties.TryGetValue(propertyNameAst.Value, out property)) + List existingValues = null; + WildcardPattern wildcardPattern = null; + bool isDependsOnProperty = String.Equals(property.Name, @"DependsOn", StringComparison.OrdinalIgnoreCase); + bool hasNewLine = false; + string stringQuote = (completionContext.TokenAtCursor is StringExpandableToken) ? "\"" : "'"; + if ((property.ValueMap != null && property.ValueMap.Count > 0) || isDependsOnProperty) { - List existingValues = null; - WildcardPattern wildcardPattern = null; - bool isDependsOnProperty = String.Equals(property.Name, @"DependsOn", StringComparison.OrdinalIgnoreCase); - bool hasNewLine = false; - string stringQuote = (completionContext.TokenAtCursor is StringExpandableToken) ? "\"" : "'"; - if ((property.ValueMap != null && property.ValueMap.Count > 0) || isDependsOnProperty) + shouldContinue = false; + existingValues = new List(); + if (String.Equals(property.TypeConstraint, "StringArray", StringComparison.OrdinalIgnoreCase)) { - shouldContinue = false; - existingValues = new List(); - if (String.Equals(property.TypeConstraint, "StringArray", StringComparison.OrdinalIgnoreCase)) + var arrayAst = Ast.GetAncestorAst(lastAst); + if (arrayAst != null && arrayAst.Elements.Count > 0) { - var arrayAst = Ast.GetAncestorAst(lastAst); - if (arrayAst != null && arrayAst.Elements.Count > 0) + foreach (ExpressionAst expression in arrayAst.Elements) { - foreach (ExpressionAst expression in arrayAst.Elements) + // + // stringAst can be null in following case + // DependsOn='[user]x',| + // + var stringAst = expression as StringConstantExpressionAst; + if (stringAst != null && IsCursorOutsideOfExtent(cursor, expression.Extent)) { - // - // stringAst can be null in following case - // DependsOn='[user]x',| - // - var stringAst = expression as StringConstantExpressionAst; - if (stringAst != null && IsCursorOutsideOfExtent(cursor, expression.Extent)) - { - existingValues.Add(stringAst.Value); - } + existingValues.Add(stringAst.Value); } } } - // - // Make sure only auto-complete string value in current line - // - stringToComplete = GetFirstLineSubString(stringToComplete, out hasNewLine); - completionContext.WordToComplete = stringToComplete; - replacementLength = completionContext.ReplacementLength = stringToComplete.Length; - // - // Calculate the replacementIndex based on cursor location (relative to the string token) - // - if (completionContext.TokenAtCursor is StringToken) - { - replacementIndex = completionContext.TokenAtCursor.Extent.StartOffset + 1; - } - else - { - replacementIndex = completionContext.CursorPosition.Offset - replacementLength; - } - completionContext.ReplacementIndex = replacementIndex; - string matchString = stringToComplete + "*"; - wildcardPattern = WildcardPattern.Get(matchString, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); - result = new List(); } + // + // Make sure only auto-complete string value in current line + // + stringToComplete = GetFirstLineSubString(stringToComplete, out hasNewLine); + completionContext.WordToComplete = stringToComplete; + replacementLength = completionContext.ReplacementLength = stringToComplete.Length; + // + // Calculate the replacementIndex based on cursor location (relative to the string token) + // + if (completionContext.TokenAtCursor is StringToken) + { + replacementIndex = completionContext.TokenAtCursor.Extent.StartOffset + 1; + } + else + { + replacementIndex = completionContext.CursorPosition.Offset - replacementLength; + } + completionContext.ReplacementIndex = replacementIndex; + string matchString = stringToComplete + "*"; + wildcardPattern = WildcardPattern.Get(matchString, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); + result = new List(); + } - Diagnostics.Assert(isCursorInString || (!hasNewLine), "hasNoQuote and hasNewLine cannot be true at the same time"); - if (property.ValueMap != null && property.ValueMap.Count > 0) + Diagnostics.Assert(isCursorInString || (!hasNewLine), "hasNoQuote and hasNewLine cannot be true at the same time"); + if (property.ValueMap != null && property.ValueMap.Count > 0) + { + IEnumerable orderedValues = property.ValueMap.Keys.OrderBy(x => x).Where(v => !existingValues.Contains(v, StringComparer.OrdinalIgnoreCase)); + var matchedResults = orderedValues.Where(v => wildcardPattern.IsMatch(v)); + if (!matchedResults.Any()) { - IEnumerable orderedValues = property.ValueMap.Keys.OrderBy(x => x).Where(v => !existingValues.Contains(v, StringComparer.OrdinalIgnoreCase)); - var matchedResults = orderedValues.Where(v => wildcardPattern.IsMatch(v)); - if (matchedResults == null || !matchedResults.Any()) - { - // Fallback to all allowed values - matchedResults = orderedValues; - } - foreach (var value in matchedResults) + // Fallback to all allowed values + matchedResults = orderedValues; + } + foreach (var value in matchedResults) + { + string completionText = isCursorInString ? value : stringQuote + value + stringQuote; + if (hasNewLine) { - string completionText = isCursorInString ? value : stringQuote + value + stringQuote; - if (hasNewLine) - completionText = completionText + stringQuote; - result.Add(new CompletionResult( - completionText, - value, - CompletionResultType.Text, - value)); + completionText = completionText + stringQuote; } + result.Add(new CompletionResult( + completionText, + value, + CompletionResultType.Text, + value)); } - else if (isDependsOnProperty) + } + else if (isDependsOnProperty) + { + var configAst = Ast.GetAncestorAst(keywordAst); + if (configAst != null) { - var configAst = Ast.GetAncestorAst(keywordAst); - if (configAst != null) + var namedBlockAst = Ast.GetAncestorAst(keywordAst); + if (namedBlockAst != null) { - var namedBlockAst = Ast.GetAncestorAst(keywordAst); - if (namedBlockAst != null) + List allResources = new List(); + foreach (var statementAst in namedBlockAst.Statements) { - List allResources = new List(); - foreach (var statementAst in namedBlockAst.Statements) + if (statementAst is DynamicKeywordStatementAst dynamicKeywordAst && + dynamicKeywordAst != keywordAst && + !String.Equals(dynamicKeywordAst.Keyword.Keyword, @"Node", StringComparison.OrdinalIgnoreCase)) { - var dynamicKeywordAst = statementAst as DynamicKeywordStatementAst; - if (dynamicKeywordAst != null && - dynamicKeywordAst != keywordAst && - !String.Equals(dynamicKeywordAst.Keyword.Keyword, @"Node", StringComparison.OrdinalIgnoreCase)) + if (!String.IsNullOrEmpty(dynamicKeywordAst.ElementName)) { - if (!String.IsNullOrEmpty(dynamicKeywordAst.ElementName)) + StringBuilder sb = new StringBuilder("[", 50); + sb.Append(dynamicKeywordAst.Keyword.Keyword); + sb.Append("]"); + sb.Append(dynamicKeywordAst.ElementName); + var resource = sb.ToString(); + if (!existingValues.Contains(resource, StringComparer.OrdinalIgnoreCase) && + !allResources.Contains(resource, StringComparer.OrdinalIgnoreCase)) { - StringBuilder sb = new StringBuilder("["); - sb.Append(dynamicKeywordAst.Keyword.Keyword); - sb.Append("]"); - sb.Append(dynamicKeywordAst.ElementName); - var resource = sb.ToString(); - if (!existingValues.Contains(resource, StringComparer.OrdinalIgnoreCase) && - !allResources.Contains(resource, StringComparer.OrdinalIgnoreCase)) - { - allResources.Add(resource); - } + allResources.Add(resource); } } } - var matchedResults = allResources.Where(r => wildcardPattern.IsMatch(r)); - if (matchedResults == null || !matchedResults.Any()) - { - // Fallback to all allowed values - matchedResults = allResources; - } + } + var matchedResults = allResources.Where(r => wildcardPattern.IsMatch(r)); + if (!matchedResults.Any()) + { + // Fallback to all allowed values + matchedResults = allResources; + } - foreach (var resource in matchedResults) - { - string completionText = isCursorInString ? resource : stringQuote + resource + stringQuote; - if (hasNewLine) - completionText = completionText + stringQuote; - result.Add(new CompletionResult( - completionText, - resource, - CompletionResultType.Text, - resource)); - } + foreach (var resource in matchedResults) + { + string completionText = isCursorInString ? resource : stringQuote + resource + stringQuote; + if (hasNewLine) + completionText = completionText + stringQuote; + result.Add(new CompletionResult( + completionText, + resource, + CompletionResultType.Text, + resource)); } } } @@ -1130,17 +1116,16 @@ private List GetResultForString(CompletionContext completionCo var tokenAtCursor = completionContext.TokenAtCursor; var lastAst = completionContext.RelatedAsts.Last(); - List result = null; var expandableString = lastAst as ExpandableStringExpressionAst; var constantString = lastAst as StringConstantExpressionAst; if (constantString == null && expandableString == null) { return null; } string strValue = constantString != null ? constantString.Value : expandableString.Value; - StringConstantType strType = constantString != null ? constantString.StringConstantType : expandableString.StringConstantType; + StringConstantType strType = constantString?.StringConstantType ?? expandableString.StringConstantType; string subInput = null; bool shouldContinue; - result = GetResultForEnumPropertyValueOfDSCResource(completionContext, strValue, ref replacementIndex, ref replacementLength, out shouldContinue); + var result = GetResultForEnumPropertyValueOfDSCResource(completionContext, strValue, ref replacementIndex, ref replacementLength, out shouldContinue); if (!shouldContinue || (result != null && result.Count > 0)) { return result; @@ -1359,49 +1344,45 @@ private List GetResultForIdentifier(CompletionContext completi completionContext.WordToComplete = ""; return CompletionCompleters.CompleteVariable(completionContext); } - else + if (strConst.Parent is UsingStatementAst usingState) { - UsingStatementAst usingState = strConst.Parent as UsingStatementAst; - if (usingState != null) + completionContext.ReplacementIndex = strConst.Extent.StartOffset; + completionContext.ReplacementLength = strConst.Extent.EndOffset - replacementIndex; + completionContext.WordToComplete = strConst.Extent.Text; + switch (usingState.UsingStatementKind) { - completionContext.ReplacementIndex = strConst.Extent.StartOffset; - completionContext.ReplacementLength = strConst.Extent.EndOffset - replacementIndex; - completionContext.WordToComplete = strConst.Extent.Text; - switch (usingState.UsingStatementKind) + case UsingStatementKind.Assembly: + break; + case UsingStatementKind.Command: + break; + case UsingStatementKind.Module: + var moduleExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + StringLiterals.PowerShellModuleFileExtension, + StringLiterals.PowerShellDataFileExtension, + StringLiterals.PowerShellNgenAssemblyExtension, + StringLiterals.DependentWorkflowAssemblyExtension, + StringLiterals.PowerShellCmdletizationFileExtension, + StringLiterals.WorkflowFileExtension + }; + result = CompletionCompleters.CompleteFilename(completionContext, false, moduleExtensions).ToList(); + if (completionContext.WordToComplete.IndexOfAny(Utils.Separators.DirectoryOrDrive) != -1) { - case UsingStatementKind.Assembly: - break; - case UsingStatementKind.Command: - break; - case UsingStatementKind.Module: - var moduleExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) - { - StringLiterals.PowerShellModuleFileExtension, - StringLiterals.PowerShellDataFileExtension, - StringLiterals.PowerShellNgenAssemblyExtension, - StringLiterals.DependentWorkflowAssemblyExtension, - StringLiterals.PowerShellCmdletizationFileExtension, - StringLiterals.WorkflowFileExtension - }; - result = CompletionCompleters.CompleteFilename(completionContext, false, moduleExtensions).ToList(); - if (completionContext.WordToComplete.IndexOfAny(Utils.Separators.DirectoryOrDrive) != -1) - { - // The partial input is a path, then we don't iterate modules under $ENV:PSModulePath - return result; - } - - var moduleResults = CompletionCompleters.CompleteModuleName(completionContext, false); - if (moduleResults != null && moduleResults.Count > 0) - result.AddRange(moduleResults); - return result; - case UsingStatementKind.Namespace: - result = CompletionCompleters.CompleteNamespace(completionContext); - return result; - case UsingStatementKind.Type: - break; - default: - throw new ArgumentOutOfRangeException("UsingStatementKind"); + // The partial input is a path, then we don't iterate modules under $ENV:PSModulePath + return result; } + + var moduleResults = CompletionCompleters.CompleteModuleName(completionContext, false); + if (moduleResults != null && moduleResults.Count > 0) + result.AddRange(moduleResults); + return result; + case UsingStatementKind.Namespace: + result = CompletionCompleters.CompleteNamespace(completionContext); + return result; + case UsingStatementKind.Type: + break; + default: + throw new ArgumentOutOfRangeException("UsingStatementKind"); } } } @@ -1476,7 +1457,7 @@ private List GetResultForIdentifier(CompletionContext completi // Handle the StringExpandableToken; var strToken = tokenAtCursor as StringExpandableToken; - if (strToken != null && strToken.NestedTokens != null && strConst != null) + if (strToken?.NestedTokens != null && strConst != null) { try { @@ -1554,7 +1535,7 @@ private List GetResultForIdentifier(CompletionContext completi TokenKind memberOperator = TokenKind.Unknown; bool isMemberCompletion = (lastAst.Parent is MemberExpressionAst); - bool isStatic = isMemberCompletion ? ((MemberExpressionAst)lastAst.Parent).Static : false; + bool isStatic = isMemberCompletion && ((MemberExpressionAst)lastAst.Parent).Static; bool isWildcard = false; if (!isMemberCompletion) @@ -1621,9 +1602,9 @@ private List GetResultForIdentifier(CompletionContext completi } } - if (lastAst.Parent is HashtableAst) + if (lastAst.Parent is HashtableAst hashtableAst) { - result = CompletionCompleters.CompleteHashtableKey(completionContext, (HashtableAst)lastAst.Parent); + result = CompletionCompleters.CompleteHashtableKey(completionContext, hashtableAst); if (result != null && result.Any()) { return result; @@ -1649,8 +1630,7 @@ private List GetResultForIdentifier(CompletionContext completi var command = lastAst.Parent as CommandBaseAst; if (command != null && command.Redirections.Any()) { - var fileRedirection = command.Redirections[0] as FileRedirectionAst; - if (fileRedirection != null && + if (command.Redirections[0] is FileRedirectionAst fileRedirection && fileRedirection.Extent.EndLineNumber == lastAst.Extent.StartLineNumber && fileRedirection.Extent.EndColumnNumber == lastAst.Extent.StartColumnNumber) { @@ -1725,7 +1705,7 @@ private List GetResultForAttributeArgument(CompletionContext c if (pro.Name != "TypeId" && (pro.Name.StartsWith(argName, StringComparison.OrdinalIgnoreCase))) { result.Add(new CompletionResult(pro.Name, pro.Name, CompletionResultType.Property, - pro.PropertyType.ToString() + " " + pro.Name)); + pro.PropertyType + " " + pro.Name)); } } return result; From 3024cd6510fecfe913aef38e23cdce6abee6ad7e Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 24 Apr 2017 08:19:48 +0200 Subject: [PATCH 5/7] Fixing styling issues Addressing review comments by iSazonov --- .../engine/CommandCompletion/CompletionAnalysis.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index bdaa209567b..4ab565898fa 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -113,8 +113,8 @@ internal CompletionContext CreateCompletionContext(ExecutionContext executionCon } else { - var stringExpandableToken = tokenAtCursor as StringExpandableToken; - if (stringExpandableToken?.NestedTokens != null) + if (tokenAtCursor is StringExpandableToken stringExpandableToken && + stringExpandableToken.NestedTokens != null) { tokenAtCursor = stringExpandableToken.NestedTokens.LastOrDefault( @@ -153,11 +153,9 @@ private static Ast GetLastAstAtCursor(ScriptBlockAst scriptBlockAst, IScriptPosi /// private static bool CompleteAgainstSwitchFile(Ast lastAst, Token tokenBeforeCursor) { - Tuple fileConditionTuple; - var errorStatement = lastAst as ErrorStatementAst; if (errorStatement?.Flags != null && errorStatement.Kind != null && tokenBeforeCursor != null && - errorStatement.Kind.Kind.Equals(TokenKind.Switch) && errorStatement.Flags.TryGetValue("file", out fileConditionTuple)) + errorStatement.Kind.Kind.Equals(TokenKind.Switch) && errorStatement.Flags.TryGetValue("file", out Tuple fileConditionTuple)) { // Handle "switch -file " return fileConditionTuple.Item1.Extent.EndOffset == tokenBeforeCursor.Extent.EndOffset; @@ -868,8 +866,7 @@ internal static TypeName FindTypeNameToComplete(ITypeName type, IScriptPosition return null; } - var arrayTypeName = type as ArrayTypeName; - if (arrayTypeName != null) + if (type is ArrayTypeName arrayTypeName) { return FindTypeNameToComplete(arrayTypeName.ElementType, cursor); } From 2727ca94c20de706d33168cd092a22caddc6940c Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Tue, 25 Apr 2017 08:37:46 +0200 Subject: [PATCH 6/7] manual merge of TabCompletion.Tests.ps1 --- .../TabCompletion/TabCompletion.Tests.ps1 | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index e69de29bb2d..a3e5f39492d 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -0,0 +1,165 @@ +Describe "TabCompletion" -Tags CI { + It 'Should complete Command' { + $res = TabExpansion2 -inputScript 'Get-Com' -cursorColumn 'Get-Com'.Length + $res.CompletionMatches[0].CompletionText | Should be Get-Command + } + + It 'Should complete native exe' -Skip:(!$IsWindows) { + $res = TabExpansion2 -inputScript 'notep' -cursorColumn 'notep'.Length + $res.CompletionMatches[0].CompletionText | Should Be notepad.exe + } + + It 'Should complete dotnet method' { + $res = TabExpansion2 -inputScript '(1).ToSt' -cursorColumn '(1).ToSt'.Length + $res.CompletionMatches[0].CompletionText | Should Be 'ToString(' + } + + It 'Should complete Magic foreach' { + $res = TabExpansion2 -inputScript '(1..10).Fo' -cursorColumn '(1..10).Fo'.Length + $res.CompletionMatches[0].CompletionText | Should Be 'Foreach(' + } + + It 'Should complete types' { + $res = TabExpansion2 -inputScript '[pscu' -cursorColumn '[pscu'.Length + $res.CompletionMatches[0].CompletionText | Should Be 'pscustomobject' + } + + It 'Should complete namespaces' { + $res = TabExpansion2 -inputScript 'using namespace Sys' -cursorColumn 'using namespace Sys'.Length + $res.CompletionMatches[0].CompletionText | Should Be 'System' + } + + It 'Should complete format-table hashtable' { + $res = TabExpansion2 -inputScript 'Get-ChildItem | Format-Table @{ ' -cursorColumn 'Get-ChildItem | Format-Table @{ '.Length + $res.CompletionMatches.Count | Should Be 5 + $res.CompletionMatches.Foreach{$_.CompletionText -in 'Label', 'Width', 'Alignment', 'Expression', 'FormatString' | Should Be $true} + } + + + It 'Should complete format-* hashtable on GroupBy' -TestCases ( + @{cmd = 'Format-Table'}, + @{cmd = 'Format-List'}, + @{cmd = 'Format-Wide'}, + @{cmd = 'Format-Custom'} + ) { + param($cmd) + $res = TabExpansion2 -inputScript "Get-ChildItem | $cmd -GroupBy @{ " -cursorColumn "Get-ChildItem | $cmd -GroupBy @{ ".Length + $res.CompletionMatches.Count | Should Be 3 + $res.CompletionMatches.Foreach{$_.CompletionText -in 'Label', 'Expression', 'FormatString' | Should Be $true} + } + + It 'Should complete format-list hashtable' { + $res = TabExpansion2 -inputScript 'Get-ChildItem | Format-List @{ ' -cursorColumn 'Get-ChildItem | Format-List @{ '.Length + $res.CompletionMatches.Count | Should Be 3 + $res.CompletionMatches.Foreach{$_.CompletionText -in 'Label', 'Expression', 'FormatString' | Should Be $true} + } + + It 'Should complete format-wide hashtable' { + $res = TabExpansion2 -inputScript 'Get-ChildItem | Format-Wide @{ ' -cursorColumn 'Get-ChildItem | Format-Wide @{ '.Length + $res.CompletionMatches.Count | Should Be 2 + $res.CompletionMatches.Foreach{$_.CompletionText -in 'Expression', 'FormatString' | Should Be $true} + } + + It 'Should complete format-custom hashtable' { + $res = TabExpansion2 -inputScript 'Get-ChildItem | Format-Custom @{ ' -cursorColumn 'Get-ChildItem | Format-Custom @{ '.Length + $res.CompletionMatches.Count | Should Be 2 + $res.CompletionMatches.Foreach{$_.CompletionText -in 'Expression', 'Depth' | Should Be $true} + } + + It 'Should complete Select-Object hashtable' { + $res = TabExpansion2 -inputScript 'Get-ChildItem | Select-Object @{ ' -cursorColumn 'Get-ChildItem | Select-Object @{ '.Length + $res.CompletionMatches.Count | Should Be 2 + $res.CompletionMatches.Foreach{$_.CompletionText -in 'Name', 'Expression' | Should Be $true} + } + + It 'Should complete Sort-Object hashtable' { + $res = TabExpansion2 -inputScript 'Get-ChildItem | Sort-Object @{ ' -cursorColumn 'Get-ChildItem | Sort-Object @{ '.Length + $res.CompletionMatches.Count | Should Be 3 + $res.CompletionMatches.Foreach{$_.CompletionText -in 'Expression', 'Ascending', 'Descending' | Should Be $true} + } + + It 'Should complete New-Object hashtable' { + class X { + $A + $B + $C + } + $res = TabExpansion2 -inputScript 'New-Object -TypeName X -Property @{ ' -cursorColumn 'New-Object -TypeName X -Property @{ '.Length + $res.CompletionMatches.Count | Should Be 3 + $res.CompletionMatches.Foreach{$_.CompletionText -in 'A', 'B', 'C' | Should Be $true} + } + + It 'Should complete keyword' -skip { + $res = TabExpansion2 -inputScript 'using nam' -cursorColumn 'using nam'.Length + $res.CompletionMatches[0].CompletionText | Should Be 'namespace' + } + + Context NativeCommand { + BeforeAll { + $nativeCommand = (Get-Command -CommandType Application -TotalCount 1).Name + } + It 'Completes native commands with -' { + Register-ArgumentCompleter -Native -CommandName $nativeCommand -ScriptBlock { + param($wordToComplete, $ast, $cursorColumn) + if ($wordToComplete -eq '-') { + return "-flag" + } + else { + return "unexpected wordtocomplete" + } + } + $line = "$nativeCommand -" + $res = TabExpansion2 -inputScript $line -cursorColumn $line.Length + $res.CompletionMatches.Count | Should Be 1 + $res.CompletionMatches.CompletionText | Should Be "-flag" + } + + It 'Completes native commands with --' { + Register-ArgumentCompleter -Native -CommandName $nativeCommand -ScriptBlock { + param($wordToComplete, $ast, $cursorColumn) + if ($wordToComplete -eq '--') { + return "--flag" + } + else { + return "unexpected wordtocomplete" + } + } + $line = "$nativeCommand --" + $res = TabExpansion2 -inputScript $line -cursorColumn $line.Length + $res.CompletionMatches.Count | Should Be 1 + $res.CompletionMatches.CompletionText | Should Be "--flag" + } + + It 'Completes native commands with --f' { + Register-ArgumentCompleter -Native -CommandName $nativeCommand -ScriptBlock { + param($wordToComplete, $ast, $cursorColumn) + if ($wordToComplete -eq '--f') { + return "--flag" + } + else { + return "unexpected wordtocomplete" + } + } + $line = "$nativeCommand --f" + $res = TaBexpansion2 -inputScript $line -cursorColumn $line.Length + $res.CompletionMatches.Count | Should Be 1 + $res.CompletionMatches.CompletionText | Should Be "--flag" + } + + It 'Completes native commands with -o' { + Register-ArgumentCompleter -Native -CommandName $nativeCommand -ScriptBlock { + param($wordToComplete, $ast, $cursorColumn) + if ($wordToComplete -eq '-o') { + return "-option" + } + else { + return "unexpected wordtocomplete" + } + } + $line = "$nativeCommand -o" + $res = TaBexpansion2 -inputScript $line -cursorColumn $line.Length + $res.CompletionMatches.Count | Should Be 1 + $res.CompletionMatches.CompletionText | Should Be "-option" + } + } +} From cfcfffa0855d7166556a8de9229fdd396dc959b9 Mon Sep 17 00:00:00 2001 From: Staffan Gustafsson Date: Mon, 22 May 2017 23:23:22 +0200 Subject: [PATCH 7/7] Removing usage of C#7 constructs --- .../CommandCompletion/CompletionAnalysis.cs | 335 ++++++++++-------- 1 file changed, 179 insertions(+), 156 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index b5c10f129d5..84bb275d41c 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -113,8 +113,8 @@ internal CompletionContext CreateCompletionContext(ExecutionContext executionCon } else { - if (tokenAtCursor is StringExpandableToken stringExpandableToken && - stringExpandableToken.NestedTokens != null) + var stringExpandableToken = tokenAtCursor as StringExpandableToken; + if (stringExpandableToken != null && stringExpandableToken.NestedTokens != null) { tokenAtCursor = stringExpandableToken.NestedTokens.LastOrDefault( @@ -153,9 +153,11 @@ private static Ast GetLastAstAtCursor(ScriptBlockAst scriptBlockAst, IScriptPosi /// private static bool CompleteAgainstSwitchFile(Ast lastAst, Token tokenBeforeCursor) { + Tuple fileConditionTuple; + var errorStatement = lastAst as ErrorStatementAst; - if (errorStatement?.Flags != null && errorStatement.Kind != null && tokenBeforeCursor != null && - errorStatement.Kind.Kind.Equals(TokenKind.Switch) && errorStatement.Flags.TryGetValue("file", out Tuple fileConditionTuple)) + if (errorStatement != null && errorStatement.Flags != null && errorStatement.Kind != null && tokenBeforeCursor != null && + errorStatement.Kind.Kind.Equals(TokenKind.Switch) && errorStatement.Flags.TryGetValue("file", out fileConditionTuple)) { // Handle "switch -file " return fileConditionTuple.Item1.Extent.EndOffset == tokenBeforeCursor.Extent.EndOffset; @@ -171,7 +173,7 @@ private static bool CompleteAgainstSwitchFile(Ast lastAst, Token tokenBeforeCurs } errorStatement = pipeline.Parent as ErrorStatementAst; - if (errorStatement?.Kind == null || errorStatement.Flags == null) + if (errorStatement == null || errorStatement.Kind == null || errorStatement.Flags == null) { return false; } @@ -204,13 +206,15 @@ private static bool CompleteAgainstStatementFlags(Ast scriptAst, Ast lastAst, To // Handle "switch -f" var errorStatement = lastAst as ErrorStatementAst; - if (errorStatement?.Kind != null) + if (errorStatement != null && errorStatement.Kind != null) { switch (errorStatement.Kind.Kind) { case TokenKind.Switch: kind = TokenKind.Switch; return true; + default: + break; } } @@ -230,12 +234,14 @@ private static bool CompleteAgainstStatementFlags(Ast scriptAst, Ast lastAst, To last = last.Parent; } - if (errorStatement?.Kind != null) + if (errorStatement != null && errorStatement.Kind != null) { switch (errorStatement.Kind.Kind) { case TokenKind.Switch: - if (errorStatement.Flags != null && errorStatement.Flags.TryGetValue(Parser.VERBATIM_ARGUMENT, out Tuple value)) + + Tuple value; + if (errorStatement.Flags != null && errorStatement.Flags.TryGetValue(Parser.VERBATIM_ARGUMENT, out value)) { if (IsTokenTheSame(value.Item1, token)) { @@ -244,6 +250,9 @@ private static bool CompleteAgainstStatementFlags(Ast scriptAst, Ast lastAst, To } } break; + + default: + break; } } } @@ -386,8 +395,9 @@ internal List GetResultHelper(CompletionContext completionCont break; case TokenKind.RBracket: - if (lastAst is TypeExpressionAst targetExpr) + if (lastAst is TypeExpressionAst) { + var targetExpr = (TypeExpressionAst)lastAst; var memberResult = new List(); CompletionCompleters.CompleteMemberHelper( @@ -479,6 +489,7 @@ internal List GetResultHelper(CompletionContext completionCont { completionContext.WordToComplete = tokenAtCursor.Text; result = CompletionCompleters.CompleteStatementFlags(statementKind, completionContext.WordToComplete); + break; } break; @@ -488,9 +499,10 @@ internal List GetResultHelper(CompletionContext completionCont ConfigurationDefinitionAst configureAst = GetAncestorConfigurationAstAndKeywordAst( completionContext.CursorPosition, lastAst, out keywordAst); Diagnostics.Assert(configureAst != null, "ConfigurationDefinitionAst should never be null"); + bool matched = false; completionContext.WordToComplete = tokenAtCursor.Text.Trim(); // Current token is within ConfigurationDefinitionAst or DynamicKeywordStatementAst - return GetResultForIdentifierInConfiguration(completionContext, configureAst, null, out _); + return GetResultForIdentifierInConfiguration(completionContext, configureAst, null, out matched); } case TokenKind.Equals: case TokenKind.AtParen: @@ -685,6 +697,8 @@ internal List GetResultHelper(CompletionContext completionCont ref replacementIndex, ref replacementLength, out unused); } break; + default: + break; } } } @@ -872,9 +886,10 @@ internal static TypeName FindTypeNameToComplete(ITypeName type, IScriptPosition return null; } - if (type is ArrayTypeName arrayTypeName) + var arrayTypeName = type as ArrayTypeName; + if (arrayTypeName != null) { - return FindTypeNameToComplete(arrayTypeName.ElementType, cursor); + return FindTypeNameToComplete(arrayTypeName.ElementType, cursor) ?? null; } return null; @@ -973,134 +988,136 @@ private List GetResultForEnumPropertyValueOfDSCResource( { IScriptPosition cursor = completionContext.CursorPosition; var keyValuePairWithCursor = GetHashEntryContainsCursor(cursor, hashTableAst, isCursorInString); - var propertyNameAst = keyValuePairWithCursor?.Item1 as StringConstantExpressionAst; - if (propertyNameAst != null) + if (keyValuePairWithCursor != null) { - DynamicKeywordProperty property; - if (keywordAst.Keyword.Properties.TryGetValue(propertyNameAst.Value, out property)) + var propertyNameAst = keyValuePairWithCursor.Item1 as StringConstantExpressionAst; + if (propertyNameAst != null) { - List existingValues = null; - WildcardPattern wildcardPattern = null; - bool isDependsOnProperty = String.Equals(property.Name, @"DependsOn", StringComparison.OrdinalIgnoreCase); - bool hasNewLine = false; - string stringQuote = (completionContext.TokenAtCursor is StringExpandableToken) ? "\"" : "'"; - if ((property.ValueMap != null && property.ValueMap.Count > 0) || isDependsOnProperty) + DynamicKeywordProperty property; + if (keywordAst.Keyword.Properties.TryGetValue(propertyNameAst.Value, out property)) { - shouldContinue = false; - existingValues = new List(); - if (String.Equals(property.TypeConstraint, "StringArray", StringComparison.OrdinalIgnoreCase)) + List existingValues = null; + WildcardPattern wildcardPattern = null; + bool isDependsOnProperty = String.Equals(property.Name, @"DependsOn", StringComparison.OrdinalIgnoreCase); + bool hasNewLine = false; + string stringQuote = (completionContext.TokenAtCursor is StringExpandableToken) ? "\"" : "'"; + if ((property.ValueMap != null && property.ValueMap.Count > 0) || isDependsOnProperty) { - var arrayAst = Ast.GetAncestorAst(lastAst); - if (arrayAst != null && arrayAst.Elements.Count > 0) + shouldContinue = false; + existingValues = new List(); + if (String.Equals(property.TypeConstraint, "StringArray", StringComparison.OrdinalIgnoreCase)) { - foreach (ExpressionAst expression in arrayAst.Elements) + var arrayAst = Ast.GetAncestorAst(lastAst); + if (arrayAst != null && arrayAst.Elements.Count > 0) { - // - // stringAst can be null in following case - // DependsOn='[user]x',| - // - var stringAst = expression as StringConstantExpressionAst; - if (stringAst != null && IsCursorOutsideOfExtent(cursor, expression.Extent)) + foreach (ExpressionAst expression in arrayAst.Elements) { - existingValues.Add(stringAst.Value); + // + // stringAst can be null in following case + // DependsOn='[user]x',| + // + var stringAst = expression as StringConstantExpressionAst; + if (stringAst != null && IsCursorOutsideOfExtent(cursor, expression.Extent)) + { + existingValues.Add(stringAst.Value); + } } } } + // + // Make sure only auto-complete string value in current line + // + stringToComplete = GetFirstLineSubString(stringToComplete, out hasNewLine); + completionContext.WordToComplete = stringToComplete; + replacementLength = completionContext.ReplacementLength = stringToComplete.Length; + // + // Calculate the replacementIndex based on cursor location (relative to the string token) + // + if (completionContext.TokenAtCursor is StringToken) + { + replacementIndex = completionContext.TokenAtCursor.Extent.StartOffset + 1; + } + else + { + replacementIndex = completionContext.CursorPosition.Offset - replacementLength; + } + completionContext.ReplacementIndex = replacementIndex; + string matchString = stringToComplete + "*"; + wildcardPattern = WildcardPattern.Get(matchString, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); + result = new List(); } - // - // Make sure only auto-complete string value in current line - // - stringToComplete = GetFirstLineSubString(stringToComplete, out hasNewLine); - completionContext.WordToComplete = stringToComplete; - replacementLength = completionContext.ReplacementLength = stringToComplete.Length; - // - // Calculate the replacementIndex based on cursor location (relative to the string token) - // - if (completionContext.TokenAtCursor is StringToken) - { - replacementIndex = completionContext.TokenAtCursor.Extent.StartOffset + 1; - } - else - { - replacementIndex = completionContext.CursorPosition.Offset - replacementLength; - } - completionContext.ReplacementIndex = replacementIndex; - string matchString = stringToComplete + "*"; - wildcardPattern = WildcardPattern.Get(matchString, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant); - result = new List(); - } - Diagnostics.Assert(isCursorInString || (!hasNewLine), "hasNoQuote and hasNewLine cannot be true at the same time"); - if (property.ValueMap != null && property.ValueMap.Count > 0) - { - IEnumerable orderedValues = property.ValueMap.Keys.OrderBy(x => x).Where(v => !existingValues.Contains(v, StringComparer.OrdinalIgnoreCase)); - var matchedResults = orderedValues.Where(v => wildcardPattern.IsMatch(v)); - if (!matchedResults.Any()) - { - // Fallback to all allowed values - matchedResults = orderedValues; - } - foreach (var value in matchedResults) + Diagnostics.Assert(isCursorInString || (!hasNewLine), "hasNoQuote and hasNewLine cannot be true at the same time"); + if (property.ValueMap != null && property.ValueMap.Count > 0) { - string completionText = isCursorInString ? value : stringQuote + value + stringQuote; - if (hasNewLine) + IEnumerable orderedValues = property.ValueMap.Keys.OrderBy(x => x).Where(v => !existingValues.Contains(v, StringComparer.OrdinalIgnoreCase)); + var matchedResults = orderedValues.Where(v => wildcardPattern.IsMatch(v)); + if (matchedResults == null || !matchedResults.Any()) { - completionText = completionText + stringQuote; + // Fallback to all allowed values + matchedResults = orderedValues; + } + foreach (var value in matchedResults) + { + string completionText = isCursorInString ? value : stringQuote + value + stringQuote; + if (hasNewLine) + completionText = completionText + stringQuote; + result.Add(new CompletionResult( + completionText, + value, + CompletionResultType.Text, + value)); } - result.Add(new CompletionResult( - completionText, - value, - CompletionResultType.Text, - value)); } - } - else if (isDependsOnProperty) - { - var configAst = Ast.GetAncestorAst(keywordAst); - if (configAst != null) + else if (isDependsOnProperty) { - var namedBlockAst = Ast.GetAncestorAst(keywordAst); - if (namedBlockAst != null) + var configAst = Ast.GetAncestorAst(keywordAst); + if (configAst != null) { - List allResources = new List(); - foreach (var statementAst in namedBlockAst.Statements) + var namedBlockAst = Ast.GetAncestorAst(keywordAst); + if (namedBlockAst != null) { - if (statementAst is DynamicKeywordStatementAst dynamicKeywordAst && - dynamicKeywordAst != keywordAst && - !String.Equals(dynamicKeywordAst.Keyword.Keyword, @"Node", StringComparison.OrdinalIgnoreCase)) + List allResources = new List(); + foreach (var statementAst in namedBlockAst.Statements) { - if (!String.IsNullOrEmpty(dynamicKeywordAst.ElementName)) + var dynamicKeywordAst = statementAst as DynamicKeywordStatementAst; + if (dynamicKeywordAst != null && + dynamicKeywordAst != keywordAst && + !String.Equals(dynamicKeywordAst.Keyword.Keyword, @"Node", StringComparison.OrdinalIgnoreCase)) { - StringBuilder sb = new StringBuilder("[", 50); - sb.Append(dynamicKeywordAst.Keyword.Keyword); - sb.Append("]"); - sb.Append(dynamicKeywordAst.ElementName); - var resource = sb.ToString(); - if (!existingValues.Contains(resource, StringComparer.OrdinalIgnoreCase) && - !allResources.Contains(resource, StringComparer.OrdinalIgnoreCase)) + if (!String.IsNullOrEmpty(dynamicKeywordAst.ElementName)) { - allResources.Add(resource); + StringBuilder sb = new StringBuilder("[", 50); + sb.Append(dynamicKeywordAst.Keyword.Keyword); + sb.Append("]"); + sb.Append(dynamicKeywordAst.ElementName); + var resource = sb.ToString(); + if (!existingValues.Contains(resource, StringComparer.OrdinalIgnoreCase) && + !allResources.Contains(resource, StringComparer.OrdinalIgnoreCase)) + { + allResources.Add(resource); + } } } } - } - var matchedResults = allResources.Where(r => wildcardPattern.IsMatch(r)); - if (!matchedResults.Any()) - { - // Fallback to all allowed values - matchedResults = allResources; - } + var matchedResults = allResources.Where(r => wildcardPattern.IsMatch(r)); + if (matchedResults == null || !matchedResults.Any()) + { + // Fallback to all allowed values + matchedResults = allResources; + } - foreach (var resource in matchedResults) - { - string completionText = isCursorInString ? resource : stringQuote + resource + stringQuote; - if (hasNewLine) - completionText = completionText + stringQuote; - result.Add(new CompletionResult( - completionText, - resource, - CompletionResultType.Text, - resource)); + foreach (var resource in matchedResults) + { + string completionText = isCursorInString ? resource : stringQuote + resource + stringQuote; + if (hasNewLine) + completionText = completionText + stringQuote; + result.Add(new CompletionResult( + completionText, + resource, + CompletionResultType.Text, + resource)); + } } } } @@ -1119,16 +1136,17 @@ private List GetResultForString(CompletionContext completionCo var tokenAtCursor = completionContext.TokenAtCursor; var lastAst = completionContext.RelatedAsts.Last(); + List result = null; var expandableString = lastAst as ExpandableStringExpressionAst; var constantString = lastAst as StringConstantExpressionAst; if (constantString == null && expandableString == null) { return null; } string strValue = constantString != null ? constantString.Value : expandableString.Value; - StringConstantType strType = constantString?.StringConstantType ?? expandableString.StringConstantType; + StringConstantType strType = constantString != null ? constantString.StringConstantType : expandableString.StringConstantType; string subInput = null; bool shouldContinue; - var result = GetResultForEnumPropertyValueOfDSCResource(completionContext, strValue, ref replacementIndex, ref replacementLength, out shouldContinue); + result = GetResultForEnumPropertyValueOfDSCResource(completionContext, strValue, ref replacementIndex, ref replacementLength, out shouldContinue); if (!shouldContinue || (result != null && result.Count > 0)) { return result; @@ -1347,45 +1365,49 @@ private List GetResultForIdentifier(CompletionContext completi completionContext.WordToComplete = ""; return CompletionCompleters.CompleteVariable(completionContext); } - if (strConst.Parent is UsingStatementAst usingState) + else { - completionContext.ReplacementIndex = strConst.Extent.StartOffset; - completionContext.ReplacementLength = strConst.Extent.EndOffset - replacementIndex; - completionContext.WordToComplete = strConst.Extent.Text; - switch (usingState.UsingStatementKind) + UsingStatementAst usingState = strConst.Parent as UsingStatementAst; + if (usingState != null) { - case UsingStatementKind.Assembly: - break; - case UsingStatementKind.Command: - break; - case UsingStatementKind.Module: - var moduleExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) - { - StringLiterals.PowerShellModuleFileExtension, - StringLiterals.PowerShellDataFileExtension, - StringLiterals.PowerShellNgenAssemblyExtension, - StringLiterals.DependentWorkflowAssemblyExtension, - StringLiterals.PowerShellCmdletizationFileExtension, - StringLiterals.WorkflowFileExtension - }; - result = CompletionCompleters.CompleteFilename(completionContext, false, moduleExtensions).ToList(); - if (completionContext.WordToComplete.IndexOfAny(Utils.Separators.DirectoryOrDrive) != -1) + completionContext.ReplacementIndex = strConst.Extent.StartOffset; + completionContext.ReplacementLength = strConst.Extent.EndOffset - replacementIndex; + completionContext.WordToComplete = strConst.Extent.Text; + switch (usingState.UsingStatementKind) { - // The partial input is a path, then we don't iterate modules under $ENV:PSModulePath - return result; - } + case UsingStatementKind.Assembly: + break; + case UsingStatementKind.Command: + break; + case UsingStatementKind.Module: + var moduleExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) + { + StringLiterals.PowerShellModuleFileExtension, + StringLiterals.PowerShellDataFileExtension, + StringLiterals.PowerShellNgenAssemblyExtension, + StringLiterals.DependentWorkflowAssemblyExtension, + StringLiterals.PowerShellCmdletizationFileExtension, + StringLiterals.WorkflowFileExtension + }; + result = CompletionCompleters.CompleteFilename(completionContext, false, moduleExtensions).ToList(); + if (completionContext.WordToComplete.IndexOfAny(Utils.Separators.DirectoryOrDrive) != -1) + { + // The partial input is a path, then we don't iterate modules under $ENV:PSModulePath + return result; + } - var moduleResults = CompletionCompleters.CompleteModuleName(completionContext, false); - if (moduleResults != null && moduleResults.Count > 0) - result.AddRange(moduleResults); - return result; - case UsingStatementKind.Namespace: - result = CompletionCompleters.CompleteNamespace(completionContext); - return result; - case UsingStatementKind.Type: - break; - default: - throw new ArgumentOutOfRangeException("UsingStatementKind"); + var moduleResults = CompletionCompleters.CompleteModuleName(completionContext, false); + if (moduleResults != null && moduleResults.Count > 0) + result.AddRange(moduleResults); + return result; + case UsingStatementKind.Namespace: + result = CompletionCompleters.CompleteNamespace(completionContext); + return result; + case UsingStatementKind.Type: + break; + default: + throw new ArgumentOutOfRangeException("UsingStatementKind"); + } } } } @@ -1460,7 +1482,7 @@ private List GetResultForIdentifier(CompletionContext completi // Handle the StringExpandableToken; var strToken = tokenAtCursor as StringExpandableToken; - if (strToken?.NestedTokens != null && strConst != null) + if (strToken != null && strToken.NestedTokens != null && strConst != null) { try { @@ -1605,9 +1627,9 @@ private List GetResultForIdentifier(CompletionContext completi } } - if (lastAst.Parent is HashtableAst hashtableAst) + if (lastAst.Parent is HashtableAst) { - result = CompletionCompleters.CompleteHashtableKey(completionContext, hashtableAst); + result = CompletionCompleters.CompleteHashtableKey(completionContext, (HashtableAst)lastAst.Parent); if (result != null && result.Any()) { return result; @@ -1633,7 +1655,8 @@ private List GetResultForIdentifier(CompletionContext completi var command = lastAst.Parent as CommandBaseAst; if (command != null && command.Redirections.Any()) { - if (command.Redirections[0] is FileRedirectionAst fileRedirection && + var fileRedirection = command.Redirections[0] as FileRedirectionAst; + if (fileRedirection != null && fileRedirection.Extent.EndLineNumber == lastAst.Extent.StartLineNumber && fileRedirection.Extent.EndColumnNumber == lastAst.Extent.StartColumnNumber) { @@ -1708,7 +1731,7 @@ private List GetResultForAttributeArgument(CompletionContext c if (pro.Name != "TypeId" && (pro.Name.StartsWith(argName, StringComparison.OrdinalIgnoreCase))) { result.Add(new CompletionResult(pro.Name, pro.Name, CompletionResultType.Property, - pro.PropertyType + " " + pro.Name)); + pro.PropertyType.ToString() + " " + pro.Name)); } } return result;