mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Add completion for index expressions for dictionaries (#17619)
This commit is contained in:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user