Add completion for index expressions for dictionaries (#17619)

This commit is contained in:
MartinGC94
2022-07-20 03:56:35 +00:00
committed by GitHub
parent 9ccaf107a0
commit 76c713d7e6
3 changed files with 174 additions and 45 deletions
@@ -503,6 +503,12 @@ namespace System.Management.Automation
return completions;
}
}
else if (lastAst.Parent is IndexExpressionAst indexExpressionAst)
{
// Handles quoted string inside index expression like: $PSVersionTable["<Tab>"]
completionContext.WordToComplete = (tokenAtCursor as StringToken).Value;
return CompletionCompleters.CompleteIndexExpression(completionContext, indexExpressionAst.Target);
}
result = GetResultForString(completionContext, ref replacementIndex, ref replacementLength, isQuotedString);
break;
@@ -748,6 +754,19 @@ namespace System.Management.Automation
result = CompletionCompleters.CompleteOperator(tokenAtCursor.Text);
break;
case TokenKind.LBracket:
if (lastAst.Parent is IndexExpressionAst indexExpression)
{
// Handles index expression with cursor right after lbracket like: $PSVersionTable[<Tab>]
completionContext.WordToComplete = string.Empty;
result = CompletionCompleters.CompleteIndexExpression(completionContext, indexExpression.Target);
if (result.Count > 0)
{
replacementIndex++;
replacementLength--;
}
}
break;
default:
if ((tokenAtCursor.TokenFlags & TokenFlags.Keyword) != 0)
{
@@ -896,6 +915,14 @@ namespace System.Management.Automation
result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);
}
break;
case TokenKind.LBracket:
if (lastAst.Parent is IndexExpressionAst indexExpression)
{
// Handles index expression where cursor is on a new line after the lbracket like: $PSVersionTable[\n<Tab>]
completionContext.WordToComplete = string.Empty;
result = CompletionCompleters.CompleteIndexExpression(completionContext, indexExpression.Target);
}
break;
default:
break;
@@ -967,6 +994,14 @@ namespace System.Management.Automation
return result;
}
break;
case TokenKind.LBracket:
if (lastAst.Parent is IndexExpressionAst indexExpression)
{
// Handles index expression with whitespace between lbracket and cursor like: $PSVersionTable[ <Tab>]
completionContext.WordToComplete = string.Empty;
result = CompletionCompleters.CompleteIndexExpression(completionContext, indexExpression.Target);
}
break;
default:
break;
}
@@ -1875,7 +1910,7 @@ namespace System.Management.Automation
return results;
}
private List<CompletionResult> GetResultForIdentifier(CompletionContext completionContext, ref int replacementIndex, ref int replacementLength, bool isQuotedString)
private static List<CompletionResult> GetResultForIdentifier(CompletionContext completionContext, ref int replacementIndex, ref int replacementLength, bool isQuotedString)
{
var tokenAtCursor = completionContext.TokenAtCursor;
var lastAst = completionContext.RelatedAsts.Last();
@@ -1957,61 +1992,65 @@ namespace System.Management.Automation
// Handle completion for a path with variable, such as: $PSHOME\ty<tab>
if (completionContext.RelatedAsts.Count > 0 && completionContext.RelatedAsts[0] is ScriptBlockAst)
{
Ast cursorAst = null;
var cursorPosition = (InternalScriptPosition)_cursorPosition;
int offsetBeforeCmdName = cursorPosition.Offset - tokenAtCursorText.Length;
if (offsetBeforeCmdName >= 0)
{
var cursorBeforeCmdName = cursorPosition.CloneWithNewOffset(offsetBeforeCmdName);
var scriptBlockAst = (ScriptBlockAst)completionContext.RelatedAsts[0];
cursorAst = GetLastAstAtCursor(scriptBlockAst, cursorBeforeCmdName);
}
Ast cursorAst = completionContext.RelatedAsts[0].FindAll(ast => ast.Extent.EndOffset <= tokenAtCursor.Extent.StartOffset, true).LastOrDefault();
if (cursorAst != null &&
cursorAst.Extent.EndLineNumber == tokenAtCursor.Extent.StartLineNumber &&
cursorAst.Extent.EndColumnNumber == tokenAtCursor.Extent.StartColumnNumber)
if (cursorAst is not null)
{
if (tokenAtCursorText.IndexOfAny(Utils.Separators.Directory) == 0)
if (cursorAst.Extent.EndOffset == tokenAtCursor.Extent.StartOffset)
{
string wordToComplete =
CompletionCompleters.ConcatenateStringPathArguments(cursorAst as CommandElementAst, tokenAtCursorText, completionContext);
if (wordToComplete != null)
if (tokenAtCursorText.IndexOfAny(Utils.Separators.Directory) == 0)
{
completionContext.WordToComplete = wordToComplete;
result = new List<CompletionResult>(CompletionCompleters.CompleteFilename(completionContext));
if (result.Count > 0)
string wordToComplete =
CompletionCompleters.ConcatenateStringPathArguments(cursorAst as CommandElementAst, tokenAtCursorText, completionContext);
if (wordToComplete != null)
{
completionContext.WordToComplete = wordToComplete;
result = new List<CompletionResult>(CompletionCompleters.CompleteFilename(completionContext));
if (result.Count > 0)
{
replacementIndex = cursorAst.Extent.StartScriptPosition.Offset;
replacementLength += cursorAst.Extent.Text.Length;
}
return result;
}
else
{
var variableAst = cursorAst as VariableExpressionAst;
string fullPath = variableAst != null
? CompletionCompleters.CombineVariableWithPartialPath(
variableAst: variableAst,
extraText: tokenAtCursorText,
executionContext: completionContext.ExecutionContext)
: null;
if (fullPath == null) { return result; }
// Continue trying the filename/commandname completion for scenarios like this: $aa\d<tab>
completionContext.WordToComplete = fullPath;
replacementIndex = cursorAst.Extent.StartScriptPosition.Offset;
replacementLength += cursorAst.Extent.Text.Length;
}
completionContext.ReplacementIndex = replacementIndex;
completionContext.ReplacementLength = replacementLength;
}
}
// Continue trying the filename/commandname completion for scenarios like this: $aa[get-<tab>
else if (cursorAst is not ErrorExpressionAst || cursorAst.Parent is not IndexExpressionAst)
{
return result;
}
else
{
var variableAst = cursorAst as VariableExpressionAst;
string fullPath = variableAst != null
? CompletionCompleters.CombineVariableWithPartialPath(
variableAst: variableAst,
extraText: tokenAtCursorText,
executionContext: completionContext.ExecutionContext)
: null;
if (fullPath == null) { return result; }
// Continue trying the filename/commandname completion for scenarios like this: $aa\d<tab>
completionContext.WordToComplete = fullPath;
replacementIndex = cursorAst.Extent.StartScriptPosition.Offset;
replacementLength += cursorAst.Extent.Text.Length;
completionContext.ReplacementIndex = replacementIndex;
completionContext.ReplacementLength = replacementLength;
}
}
// Continue trying the filename/commandname completion for scenarios like this: $aa[get-<tab>
else if (cursorAst is not ErrorExpressionAst || cursorAst.Parent is not IndexExpressionAst)
if (cursorAst.Parent is IndexExpressionAst indexExpression && indexExpression.Index is ErrorExpressionAst)
{
return result;
if (completionContext.WordToComplete.EndsWith(']'))
{
completionContext.WordToComplete = completionContext.WordToComplete.Remove(completionContext.WordToComplete.Length - 1);
}
// Handles index expression with unquoted word like: $PSVersionTable[psver<Tab>]
return CompletionCompleters.CompleteIndexExpression(completionContext, indexExpression.Target);
}
}
}
@@ -5950,6 +5950,42 @@ namespace System.Management.Automation
return Ast.GetAncestorAst<ConfigurationDefinitionAst>(expression) != null;
}
internal static List<CompletionResult> CompleteIndexExpression(CompletionContext context, ExpressionAst indexTarget)
{
var result = new List<CompletionResult>();
object value;
if (SafeExprEvaluator.TrySafeEval(indexTarget, context.ExecutionContext, out value)
&& value is not null
&& PSObject.Base(value) is IDictionary dictionary)
{
foreach (var key in dictionary.Keys)
{
if (key is string keyAsString && keyAsString.StartsWith(context.WordToComplete, StringComparison.OrdinalIgnoreCase))
{
result.Add(new CompletionResult($"'{keyAsString}'", keyAsString, CompletionResultType.Property, keyAsString));
}
}
}
else
{
var inferredTypes = AstTypeInference.InferTypeOf(indexTarget, context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval);
foreach (var type in inferredTypes)
{
if (type is PSSyntheticTypeName synthetic)
{
foreach (var member in synthetic.Members)
{
if (member.Name.StartsWith(context.WordToComplete, StringComparison.OrdinalIgnoreCase))
{
result.Add(new CompletionResult($"'{member.Name}'", member.Name, CompletionResultType.Property, member.Name));
}
}
}
}
}
return result;
}
private static void CompleteFormatViewByInferredType(CompletionContext context, string[] inferredTypeNames, List<CompletionResult> results, string commandName)
{
var typeInfoDB = context.TypeInferenceContext.ExecutionContext.FormatDBManager.GetTypeInfoDataBase();
@@ -6116,7 +6152,19 @@ namespace System.Management.Automation
}
var completionResultType = isMethod ? CompletionResultType.Method : CompletionResultType.Property;
var completionText = isMethod ? memberName + "(" : memberName;
string completionText;
if (isMethod)
{
completionText = $"{memberName}(";
}
else if (memberName.IndexOfAny(s_charactersRequiringQuotes) != -1)
{
completionText = $"'{memberName}'";
}
else
{
completionText = memberName;
}
results.Add(new CompletionResult(completionText, memberName, completionResultType, getToolTip()));
}
@@ -82,6 +82,48 @@ Describe "TabCompletion" -Tags CI {
}
}
It 'should complete index expression for <Intent>' -TestCases @(
@{
Intent = 'Hashtable with no user input'
Expected = "'PSVersion'"
TestString = '$PSVersionTable[^'
}
@{
Intent = 'Hashtable with partial input'
Expected = "'PSVersion'"
TestString = '$PSVersionTable[ PSvers^'
}
@{
Intent = 'Hashtable with partial quoted input'
Expected = "'PSVersion'"
TestString = '$PSVersionTable["PSvers^'
}
@{
Intent = 'Hashtable from Ast'
Expected = "'Hello'"
TestString = '$Table = @{Hello = "World"};$Table[^'
}
@{
Intent = 'Hashtable with cursor on new line'
Expected = "'Hello'"
TestString = @'
$Table = @{Hello = "World"}
$Table[
^
'@
}
) -Test {
param($Expected, $TestString)
$CursorIndex = $TestString.IndexOf('^')
$res = TabExpansion2 -cursorColumn $CursorIndex -inputScript $TestString.Remove($CursorIndex, 1)
$res.CompletionMatches[0].CompletionText | Should -BeExactly $Expected
}
it 'should add quotes when completing hashtable key from Ast with member syntax' -Test {
$res = TabExpansion2 -inputScript '$Table = @{"Hello World" = "World"};$Table.'
$res.CompletionMatches.CompletionText | Where-Object {$_ -eq "'Hello World'"} | Should -BeExactly "'Hello World'"
}
It '<Intent>' -TestCases @(
@{
Intent = 'Complete member with space between dot and cursor'