PowerShell/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs

2210 lines
104 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation.Language;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Management.Automation.Subsystem;
using System.Management.Automation.Subsystem.DSC;
namespace System.Management.Automation
{
internal class CompletionContext
{
internal List<Ast> RelatedAsts { get; set; }
// Only one of TokenAtCursor or TokenBeforeCursor is set
// This is how we can tell if we're trying to complete part of something (like a member)
// or complete an argument, where TokenBeforeCursor could be a parameter name.
internal Token TokenAtCursor { get; set; }
internal Token TokenBeforeCursor { get; set; }
internal IScriptPosition CursorPosition { get; set; }
internal PowerShellExecutionHelper Helper { get; set; }
internal Hashtable Options { get; set; }
internal Dictionary<string, ScriptBlock> CustomArgumentCompleters { get; set; }
internal Dictionary<string, ScriptBlock> NativeArgumentCompleters { get; set; }
internal string WordToComplete { get; set; }
internal int ReplacementIndex { get; set; }
internal int ReplacementLength { get; set; }
internal ExecutionContext ExecutionContext { get; set; }
internal PseudoBindingInfo PseudoBindingInfo { get; set; }
internal TypeInferenceContext TypeInferenceContext { get; set; }
internal bool GetOption(string option, bool @default)
{
if (Options == null || !Options.ContainsKey(option))
{
return @default;
}
return LanguagePrimitives.ConvertTo<bool>(Options[option]);
}
}
internal class CompletionAnalysis
{
private readonly Ast _ast;
private readonly Token[] _tokens;
private readonly IScriptPosition _cursorPosition;
private readonly Hashtable _options;
internal CompletionAnalysis(Ast ast, Token[] tokens, IScriptPosition cursorPosition, Hashtable options)
{
_ast = ast;
_tokens = tokens;
_cursorPosition = cursorPosition;
_options = options;
}
private static bool IsInterestingToken(Token token)
{
return token.Kind != TokenKind.NewLine && token.Kind != TokenKind.EndOfInput;
}
private static bool IsCursorWithinOrJustAfterExtent(IScriptPosition cursor, IScriptExtent extent)
{
return cursor.Offset > extent.StartOffset && cursor.Offset <= extent.EndOffset;
}
private static bool IsCursorRightAfterExtent(IScriptPosition cursor, IScriptExtent extent)
{
return cursor.Offset == extent.EndOffset;
}
private static bool IsCursorAfterExtentAndInTheSameLine(IScriptPosition cursor, IScriptExtent extent)
{
return cursor.Offset >= extent.EndOffset && extent.EndLineNumber == cursor.LineNumber;
}
private static bool IsCursorBeforeExtent(IScriptPosition cursor, IScriptExtent extent)
{
return cursor.Offset < extent.StartOffset;
}
private static bool IsCursorAfterExtent(IScriptPosition cursor, IScriptExtent extent)
{
return extent.EndOffset < cursor.Offset;
}
private static bool IsCursorOutsideOfExtent(IScriptPosition cursor, IScriptExtent extent)
{
return cursor.Offset < extent.StartOffset || cursor.Offset > extent.EndOffset;
}
internal readonly struct AstAnalysisContext
{
internal AstAnalysisContext(Token tokenAtCursor, Token tokenBeforeCursor, List<Ast> relatedAsts, int replacementIndex)
{
TokenAtCursor = tokenAtCursor;
TokenBeforeCursor = tokenBeforeCursor;
RelatedAsts = relatedAsts;
ReplacementIndex = replacementIndex;
}
internal readonly Token TokenAtCursor;
internal readonly Token TokenBeforeCursor;
internal readonly List<Ast> RelatedAsts;
internal readonly int ReplacementIndex;
}
internal static AstAnalysisContext ExtractAstContext(Ast inputAst, Token[] inputTokens, IScriptPosition cursor)
{
bool adjustLineAndColumn = false;
IScriptPosition positionForAstSearch = cursor;
Token tokenBeforeCursor = null;
Token tokenAtCursor = InterstingTokenAtCursorOrDefault(inputTokens, cursor);
if (tokenAtCursor == null)
{
tokenBeforeCursor = InterstingTokenBeforeCursorOrDefault(inputTokens, cursor);
if (tokenBeforeCursor != null)
{
positionForAstSearch = tokenBeforeCursor.Extent.EndScriptPosition;
adjustLineAndColumn = true;
}
}
else
{
var stringExpandableToken = tokenAtCursor as StringExpandableToken;
if (stringExpandableToken?.NestedTokens != null)
{
tokenAtCursor = InterstingTokenAtCursorOrDefault(stringExpandableToken.NestedTokens, cursor) ?? stringExpandableToken;
}
}
int replacementIndex = adjustLineAndColumn ? cursor.Offset : 0;
List<Ast> relatedAsts = AstSearcher.FindAll(
inputAst,
ast => IsCursorWithinOrJustAfterExtent(positionForAstSearch, ast.Extent),
searchNestedScriptBlocks: true).ToList();
Diagnostics.Assert(tokenAtCursor == null || tokenBeforeCursor == null, "Only one of these tokens can be non-null");
return new AstAnalysisContext(tokenAtCursor, tokenBeforeCursor, relatedAsts, replacementIndex);
}
internal CompletionContext CreateCompletionContext(PowerShell powerShell)
{
var typeInferenceContext = new TypeInferenceContext(powerShell);
return InitializeCompletionContext(typeInferenceContext);
}
internal CompletionContext CreateCompletionContext(TypeInferenceContext typeInferenceContext)
{
return InitializeCompletionContext(typeInferenceContext);
}
private CompletionContext InitializeCompletionContext(TypeInferenceContext typeInferenceContext)
{
var astContext = ExtractAstContext(_ast, _tokens, _cursorPosition);
if (typeInferenceContext.CurrentTypeDefinitionAst == null)
{
typeInferenceContext.CurrentTypeDefinitionAst = Ast.GetAncestorTypeDefinitionAst(astContext.RelatedAsts.Last());
}
ExecutionContext executionContext = typeInferenceContext.ExecutionContext;
return new CompletionContext
{
Options = _options,
CursorPosition = _cursorPosition,
TokenAtCursor = astContext.TokenAtCursor,
TokenBeforeCursor = astContext.TokenBeforeCursor,
RelatedAsts = astContext.RelatedAsts,
ReplacementIndex = astContext.ReplacementIndex,
ExecutionContext = executionContext,
TypeInferenceContext = typeInferenceContext,
Helper = typeInferenceContext.Helper,
CustomArgumentCompleters = executionContext.CustomArgumentCompleters,
NativeArgumentCompleters = executionContext.NativeArgumentCompleters,
};
}
private static Token InterstingTokenAtCursorOrDefault(IReadOnlyList<Token> tokens, IScriptPosition cursorPosition)
{
for (int i = tokens.Count - 1; i >= 0; --i)
{
Token token = tokens[i];
if (IsCursorWithinOrJustAfterExtent(cursorPosition, token.Extent) && IsInterestingToken(token))
{
return token;
}
}
return null;
}
private static Token InterstingTokenBeforeCursorOrDefault(IReadOnlyList<Token> tokens, IScriptPosition cursorPosition)
{
for (int i = tokens.Count - 1; i >= 0; --i)
{
Token token = tokens[i];
if (IsCursorAfterExtent(cursorPosition, token.Extent) && IsInterestingToken(token))
{
return token;
}
}
return null;
}
private static Ast GetLastAstAtCursor(ScriptBlockAst scriptBlockAst, IScriptPosition cursorPosition)
{
var asts = AstSearcher.FindAll(scriptBlockAst, ast => IsCursorRightAfterExtent(cursorPosition, ast.Extent), searchNestedScriptBlocks: true);
return asts.LastOrDefault();
}
#region Special Cases
/// <summary>
/// Check if we should complete file names for "switch -file"
/// </summary>
private static bool CompleteAgainstSwitchFile(Ast lastAst, Token tokenBeforeCursor)
{
Tuple<Token, Ast> fileConditionTuple;
var errorStatement = lastAst as ErrorStatementAst;
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 <tab>"
return fileConditionTuple.Item1.Extent.EndOffset == tokenBeforeCursor.Extent.EndOffset;
}
if (lastAst.Parent is CommandExpressionAst)
{
// Handle "switch -file m<tab>" or "switch -file *.ps1<tab>"
if (!(lastAst.Parent.Parent is PipelineAst pipeline))
{
return false;
}
errorStatement = pipeline.Parent as ErrorStatementAst;
if (errorStatement == null || errorStatement.Kind == null || errorStatement.Flags == null)
{
return false;
}
return (errorStatement.Kind.Kind.Equals(TokenKind.Switch) &&
errorStatement.Flags.TryGetValue("file", out fileConditionTuple) && fileConditionTuple.Item2 == pipeline);
}
return false;
}
private static bool CompleteOperator(Token tokenAtCursor, Ast lastAst)
{
if (tokenAtCursor.Kind == TokenKind.Minus)
{
return lastAst is BinaryExpressionAst;
}
else if (tokenAtCursor.Kind == TokenKind.Parameter)
{
if (lastAst is CommandParameterAst)
return lastAst.Parent is ExpressionAst;
}
return false;
}
private static bool CompleteAgainstStatementFlags(Ast scriptAst, Ast lastAst, Token token, out TokenKind kind)
{
kind = TokenKind.Unknown;
// Handle "switch -f<tab>"
var errorStatement = lastAst as ErrorStatementAst;
if (errorStatement != null && errorStatement.Kind != null)
{
switch (errorStatement.Kind.Kind)
{
case TokenKind.Switch:
kind = TokenKind.Switch;
return true;
default:
break;
}
}
// Handle "switch -<tab>". Skip cases like "switch ($a) {} -<tab> "
var scriptBlockAst = scriptAst as ScriptBlockAst;
if (token != null && token.Kind == TokenKind.Minus && scriptBlockAst != null)
{
var asts = AstSearcher.FindAll(scriptBlockAst, ast => IsCursorAfterExtent(token.Extent.StartScriptPosition, ast.Extent), searchNestedScriptBlocks: true);
Ast last = asts.LastOrDefault();
errorStatement = null;
while (last != null)
{
errorStatement = last as ErrorStatementAst;
if (errorStatement != null) { break; }
last = last.Parent;
}
if (errorStatement != null && errorStatement.Kind != null)
{
switch (errorStatement.Kind.Kind)
{
case TokenKind.Switch:
Tuple<Token, Ast> value;
if (errorStatement.Flags != null && errorStatement.Flags.TryGetValue(Parser.VERBATIM_ARGUMENT, out value))
{
if (IsTokenTheSame(value.Item1, token))
{
kind = TokenKind.Switch;
return true;
}
}
break;
default:
break;
}
}
}
return false;
}
private static bool IsTokenTheSame(Token x, Token y)
{
if (x.Kind == y.Kind && x.TokenFlags == y.TokenFlags &&
x.Extent.StartLineNumber == y.Extent.StartLineNumber &&
x.Extent.StartColumnNumber == y.Extent.StartColumnNumber &&
x.Extent.EndLineNumber == y.Extent.EndLineNumber &&
x.Extent.EndColumnNumber == y.Extent.EndColumnNumber)
{
return true;
}
return false;
}
#endregion Special Cases
internal List<CompletionResult> GetResults(PowerShell powerShell, out int replacementIndex, out int replacementLength)
{
var completionContext = CreateCompletionContext(powerShell);
PSLanguageMode? previousLanguageMode = null;
try
{
// Tab expansion is called from a trusted function - we should apply ConstrainedLanguage if necessary.
if (completionContext.ExecutionContext.HasRunspaceEverUsedConstrainedLanguageMode)
{
previousLanguageMode = completionContext.ExecutionContext.LanguageMode;
completionContext.ExecutionContext.LanguageMode = PSLanguageMode.ConstrainedLanguage;
}
return GetResultHelper(completionContext, out replacementIndex, out replacementLength, false);
}
finally
{
if (previousLanguageMode.HasValue)
{
completionContext.ExecutionContext.LanguageMode = previousLanguageMode.Value;
}
}
}
internal List<CompletionResult> GetResultHelper(CompletionContext completionContext, out int replacementIndex, out int replacementLength, bool isQuotedString)
{
replacementIndex = -1;
replacementLength = -1;
var tokenAtCursor = completionContext.TokenAtCursor;
var lastAst = completionContext.RelatedAsts.Last();
List<CompletionResult> result = null;
if (tokenAtCursor != null)
{
replacementIndex = tokenAtCursor.Extent.StartScriptPosition.Offset;
replacementLength = tokenAtCursor.Extent.EndScriptPosition.Offset - replacementIndex;
completionContext.ReplacementIndex = replacementIndex;
completionContext.ReplacementLength = replacementLength;
switch (tokenAtCursor.Kind)
{
case TokenKind.Variable:
case TokenKind.SplattedVariable:
completionContext.WordToComplete = ((VariableToken)tokenAtCursor).VariablePath.UserPath;
result = CompletionCompleters.CompleteVariable(completionContext);
break;
case TokenKind.Multiply:
case TokenKind.Generic:
case TokenKind.MinusMinus: // for native commands '--'
case TokenKind.Identifier:
result = GetResultForIdentifier(completionContext, ref replacementIndex, ref replacementLength, isQuotedString);
break;
case TokenKind.Parameter:
// When it's the content of a quoted string, we only handle variable/member completion
if (isQuotedString)
break;
completionContext.WordToComplete = tokenAtCursor.Text;
var cmdAst = lastAst.Parent as CommandAst;
if (lastAst is StringConstantExpressionAst && cmdAst != null && cmdAst.CommandElements.Count == 1)
{
result = CompleteFileNameAsCommand(completionContext);
break;
}
TokenKind statementKind;
if (CompleteAgainstStatementFlags(null, lastAst, null, out statementKind))
{
result = CompletionCompleters.CompleteStatementFlags(statementKind, completionContext.WordToComplete);
break;
}
if (CompleteOperator(tokenAtCursor, lastAst))
{
result = CompletionCompleters.CompleteOperator(completionContext.WordToComplete);
break;
}
// Handle scenarios like this: dir -path:<tab>
if (completionContext.WordToComplete.EndsWith(':'))
{
replacementIndex = tokenAtCursor.Extent.EndScriptPosition.Offset;
replacementLength = 0;
completionContext.WordToComplete = string.Empty;
result = CompletionCompleters.CompleteCommandArgument(completionContext);
}
else
{
result = CompletionCompleters.CompleteCommandParameter(completionContext);
}
break;
case TokenKind.Dot:
case TokenKind.ColonColon:
case TokenKind.QuestionDot:
replacementIndex += tokenAtCursor.Text.Length;
replacementLength = 0;
result = CompletionCompleters.CompleteMember(completionContext, @static: tokenAtCursor.Kind == TokenKind.ColonColon);
break;
case TokenKind.Comment:
// When it's the content of a quoted string, we only handle variable/member completion
if (isQuotedString)
break;
completionContext.WordToComplete = tokenAtCursor.Text;
result = CompletionCompleters.CompleteComment(completionContext, ref replacementIndex, ref replacementLength);
break;
case TokenKind.StringExpandable:
case TokenKind.StringLiteral:
// Search to see if we're looking at an assignment
if (lastAst.Parent is CommandExpressionAst
&& lastAst.Parent.Parent is AssignmentStatementAst assignmentAst)
{
// Handle scenarios like `$ErrorActionPreference = '<tab>`
if (TryGetCompletionsForVariableAssignment(completionContext, assignmentAst, out List<CompletionResult> completions))
{
return completions;
}
}
result = GetResultForString(completionContext, ref replacementIndex, ref replacementLength, isQuotedString);
break;
case TokenKind.RBracket:
if (lastAst is TypeExpressionAst)
{
var targetExpr = (TypeExpressionAst)lastAst;
var memberResult = new List<CompletionResult>();
CompletionCompleters.CompleteMemberHelper(
true,
"*",
targetExpr,
completionContext, memberResult);
if (memberResult.Count > 0)
{
replacementIndex++;
replacementLength = 0;
result = (from entry in memberResult
let completionText = TokenKind.ColonColon.Text() + entry.CompletionText
select new CompletionResult(completionText, entry.ListItemText, entry.ResultType, entry.ToolTip)).ToList();
}
}
break;
case TokenKind.Comma:
// Handle array elements such as dir .\cd,<tab> || dir -Path: .\cd,<tab>
if (lastAst is ErrorExpressionAst &&
(lastAst.Parent is CommandAst || lastAst.Parent is CommandParameterAst))
{
replacementIndex += replacementLength;
replacementLength = 0;
result = CompletionCompleters.CompleteCommandArgument(completionContext);
}
else if (lastAst is AttributeAst)
{
completionContext.ReplacementIndex = replacementIndex += tokenAtCursor.Text.Length;
completionContext.ReplacementLength = replacementLength = 0;
result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);
}
else
{
//
// Handle auto completion for enum/dependson property of DSC resource,
// cursor is right after ','
//
// Configuration config
// {
// User test
// {
// DependsOn=@('[user]x',|)
//
bool unused;
result = GetResultForEnumPropertyValueOfDSCResource(completionContext, string.Empty, ref replacementIndex, ref replacementLength, out unused);
}
break;
case TokenKind.AtCurly:
// Handle scenarios such as 'Sort-Object @{<tab>' and 'gci | Format-Table @{'
result = GetResultForHashtable(completionContext);
replacementIndex += 2;
replacementLength = 0;
break;
case TokenKind.Semi:
// Handle scenarios such as 'gci | Format-Table @{Label=...;<tab>'
if (lastAst is HashtableAst)
{
result = GetResultForHashtable(completionContext);
replacementIndex += 1;
replacementLength = 0;
}
break;
case TokenKind.Number:
// Handle scenarios such as Get-Process -Id 5<tab> || Get-Process -Id 5210, 3<tab> || Get-Process -Id: 5210, 3<tab>
if (lastAst is ConstantExpressionAst &&
(lastAst.Parent is CommandAst || lastAst.Parent is CommandParameterAst ||
(lastAst.Parent is ArrayLiteralAst &&
(lastAst.Parent.Parent is CommandAst || lastAst.Parent.Parent is CommandParameterAst))))
{
completionContext.WordToComplete = tokenAtCursor.Text;
result = CompletionCompleters.CompleteCommandArgument(completionContext);
replacementIndex = completionContext.ReplacementIndex;
replacementLength = completionContext.ReplacementLength;
}
else if (lastAst.Parent is CommandExpressionAst
&& lastAst.Parent.Parent is AssignmentStatementAst assignmentAst2)
{
// Handle scenarios like '[ValidateSet(11,22)][int]$i = 11; $i = 2<tab>'
if (TryGetCompletionsForVariableAssignment(completionContext, assignmentAst2, out List<CompletionResult> completions))
{
result = completions;
}
}
break;
case TokenKind.Redirection:
// Handle file name completion after the redirection operator: gps ><tab> || gps >><tab> || dir con 2><tab> || dir con 2>><tab>
if (lastAst is ErrorExpressionAst && lastAst.Parent is FileRedirectionAst)
{
completionContext.WordToComplete = string.Empty;
completionContext.ReplacementIndex = (replacementIndex += tokenAtCursor.Text.Length);
completionContext.ReplacementLength = replacementLength = 0;
result = new List<CompletionResult>(CompletionCompleters.CompleteFilename(completionContext));
}
break;
case TokenKind.Minus:
// Handle operator completion: 55 -<tab> || "string" -<tab> || (Get-Something) -<tab>
if (CompleteOperator(tokenAtCursor, lastAst))
{
result = CompletionCompleters.CompleteOperator(string.Empty);
break;
}
// Handle the flag completion for statements, such as the switch statement
if (CompleteAgainstStatementFlags(completionContext.RelatedAsts[0], null, tokenAtCursor, out statementKind))
{
completionContext.WordToComplete = tokenAtCursor.Text;
result = CompletionCompleters.CompleteStatementFlags(statementKind, completionContext.WordToComplete);
break;
}
break;
case TokenKind.DynamicKeyword:
{
DynamicKeywordStatementAst keywordAst;
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);
}
case TokenKind.Equals:
case TokenKind.AtParen:
case TokenKind.LParen:
{
if (lastAst is AttributeAst)
{
completionContext.ReplacementIndex = replacementIndex += tokenAtCursor.Text.Length;
completionContext.ReplacementLength = replacementLength = 0;
result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);
}
else if (lastAst is HashtableAst hashTableAst && lastAst.Parent is not DynamicKeywordStatementAst && CheckForPendingAssignment(hashTableAst))
{
// Handle scenarios such as 'gci | Format-Table @{Label=<tab>' if incomplete parsing of the assignment.
return null;
}
else if (lastAst is AssignmentStatementAst assignmentAst2)
{
completionContext.ReplacementIndex = replacementIndex += tokenAtCursor.Text.Length;
completionContext.ReplacementLength = replacementLength = 0;
// Handle scenarios like '$ErrorActionPreference =<tab>'
if (TryGetCompletionsForVariableAssignment(completionContext, assignmentAst2, out List<CompletionResult> completions))
{
return completions;
}
}
else
{
// Handle scenarios such as 'configuration foo { File ab { Attributes ='
// (auto completion for enum/dependson property of DSC resource),
// cursor is right after '=', '(' or '@('
//
// Configuration config
// {
// User test
// {
// DependsOn=|
// DependsOn=@(|)
// DependsOn=(|
//
bool unused;
result = GetResultForEnumPropertyValueOfDSCResource(completionContext, string.Empty, ref replacementIndex, ref replacementLength, out unused);
}
break;
}
default:
if ((tokenAtCursor.TokenFlags & TokenFlags.Keyword) != 0)
{
completionContext.WordToComplete = tokenAtCursor.Text;
// Handle the file name completion
result = CompleteFileNameAsCommand(completionContext);
// Handle the command name completion
var commandNameResult = CompletionCompleters.CompleteCommand(completionContext);
if (commandNameResult != null && commandNameResult.Count > 0)
{
result.AddRange(commandNameResult);
}
}
else
{
replacementIndex = -1;
replacementLength = -1;
}
break;
}
}
else
{
IScriptPosition cursor = completionContext.CursorPosition;
bool isCursorLineEmpty = string.IsNullOrWhiteSpace(cursor.Line);
var tokenBeforeCursor = completionContext.TokenBeforeCursor;
bool isLineContinuationBeforeCursor = false;
if (tokenBeforeCursor != null)
{
//
// Handle following scenario, cursor is in next line and after a command call,
// we need to skip the command call autocompletion if there is no backtick character
// in the end of the previous line, since backtick means command call continues to the next line
//
// Configuration config
// {
// User test
// {
// DependsOn=zzz
// |
//
isLineContinuationBeforeCursor = completionContext.TokenBeforeCursor.Kind == TokenKind.LineContinuation;
}
bool skipAutoCompleteForCommandCall = isCursorLineEmpty && !isLineContinuationBeforeCursor;
bool lastAstIsExpressionAst = lastAst is ExpressionAst;
if (!isQuotedString &&
!skipAutoCompleteForCommandCall &&
(lastAst is CommandParameterAst || lastAst is CommandAst ||
(lastAstIsExpressionAst && lastAst.Parent is CommandAst) ||
(lastAstIsExpressionAst && lastAst.Parent is CommandParameterAst) ||
(lastAstIsExpressionAst && lastAst.Parent is ArrayLiteralAst &&
(lastAst.Parent.Parent is CommandAst || lastAst.Parent.Parent is CommandParameterAst))))
{
completionContext.WordToComplete = string.Empty;
var hashTableAst = lastAst as HashtableAst;
// Do not do any tab completion if we have a hash table
// and an assignment is pending. For cases like:
// new-object System.Drawing.Point -prop @{ X= -> Tab should not complete
// Note: This check works when all statements preceding the last are complete,
// but if a preceding statement is incomplete this test fails because
// the Ast mixes the statements due to incomplete parsing.
// e.g.,
// new-object System.Drawing.Point -prop @{ X = 100; Y = <- Incomplete line
// new-object new-object System.Drawing.Point -prop @{ X = <- Tab will yield hash properties.
if (hashTableAst != null &&
CheckForPendingAssignment(hashTableAst))
{
return result;
}
if (hashTableAst != null)
{
completionContext.ReplacementIndex = replacementIndex = completionContext.CursorPosition.Offset;
completionContext.ReplacementLength = replacementLength = 0;
result = CompletionCompleters.CompleteHashtableKey(completionContext, hashTableAst);
}
else
{
result = CompletionCompleters.CompleteCommandArgument(completionContext);
replacementIndex = completionContext.ReplacementIndex;
replacementLength = completionContext.ReplacementLength;
}
}
else if (!isQuotedString)
{
//
// Handle completion of empty line within configuration statement
// Ignore the auto completion if there is a backtick character in previous line
//
bool cursorAtLineContinuation;
if ((tokenAtCursor != null && tokenAtCursor.Kind == TokenKind.LineContinuation) ||
(tokenBeforeCursor != null && tokenBeforeCursor.Kind == TokenKind.LineContinuation))
cursorAtLineContinuation = true;
else
cursorAtLineContinuation = false;
if (isCursorLineEmpty && !cursorAtLineContinuation)
{
//
// Handle following scenario, both Configuration and DSC resource 'User' are not complete
// Check Hashtable first, and then fallback to configuration
//
// Configuration config
// {
// User test
// {
// DependsOn=''
// |
result = GetResultForHashtable(completionContext);
if (result == null || result.Count == 0)
{
DynamicKeywordStatementAst keywordAst;
ConfigurationDefinitionAst configAst = GetAncestorConfigurationAstAndKeywordAst(cursor, lastAst, out keywordAst);
if (configAst != null)
{
bool matched;
result = GetResultForIdentifierInConfiguration(completionContext, configAst, keywordAst, out matched);
}
}
}
else if (completionContext.TokenAtCursor == null)
{
if (tokenBeforeCursor != null)
{
//
// Handle auto completion for enum/dependson property of DSC resource,
// cursor is after '=', ',', '(', or '@('
//
// Configuration config
// {
// User test
// {
// DependsOn= |
// DependsOn=@('[user]x', |)
// DependsOn=@( |)
// DependsOn=(|
//
switch (tokenBeforeCursor.Kind)
{
case TokenKind.Equals:
case TokenKind.Comma:
case TokenKind.AtParen:
case TokenKind.LParen:
{
if (lastAst is AssignmentStatementAst assignmentAst)
{
// Handle scenarios like '$ErrorActionPreference = <tab>'
if (TryGetCompletionsForVariableAssignment(completionContext, assignmentAst, out result))
{
break;
}
}
if (lastAst is AttributeAst)
{
completionContext.ReplacementLength = replacementLength = 0;
result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);
break;
}
bool unused;
result = GetResultForEnumPropertyValueOfDSCResource(completionContext, string.Empty, ref replacementIndex, ref replacementLength, out unused);
break;
}
default:
break;
}
}
}
if (result != null && result.Count > 0)
{
completionContext.ReplacementIndex = replacementIndex = completionContext.CursorPosition.Offset;
completionContext.ReplacementLength = replacementLength = 0;
}
else
{
bool needFileCompletion = false;
if (lastAst is ErrorExpressionAst && lastAst.Parent is FileRedirectionAst)
{
// Handle file name completion after redirection operator: gps > <tab>
needFileCompletion = true;
}
else if (lastAst is ErrorStatementAst && CompleteAgainstSwitchFile(lastAst, completionContext.TokenBeforeCursor))
{
// Handle file name completion after "switch -file": switch -file <tab>
needFileCompletion = true;
}
if (needFileCompletion)
{
completionContext.WordToComplete = string.Empty;
result = new List<CompletionResult>(CompletionCompleters.CompleteFilename(completionContext));
replacementIndex = completionContext.ReplacementIndex;
replacementLength = completionContext.ReplacementLength;
}
}
}
}
if (result == null || result.Count == 0)
{
var typeAst = completionContext.RelatedAsts.OfType<TypeExpressionAst>().FirstOrDefault();
TypeName typeNameToComplete = null;
if (typeAst != null)
{
typeNameToComplete = FindTypeNameToComplete(typeAst.TypeName, _cursorPosition);
}
else
{
var typeConstraintAst = completionContext.RelatedAsts.OfType<TypeConstraintAst>().FirstOrDefault();
if (typeConstraintAst != null)
{
typeNameToComplete = FindTypeNameToComplete(typeConstraintAst.TypeName, _cursorPosition);
}
}
if (typeNameToComplete != null)
{
// See if the typename to complete really is within the typename, and if so, which one, in the case of generics.
replacementIndex = typeNameToComplete.Extent.StartOffset;
replacementLength = typeNameToComplete.Extent.EndOffset - replacementIndex;
completionContext.WordToComplete = typeNameToComplete.FullName;
result = CompletionCompleters.CompleteType(completionContext);
}
}
if (result == null || result.Count == 0)
{
result = GetResultForHashtable(completionContext);
}
if (result == null || result.Count == 0)
{
// Handle special file completion scenarios: .\+file.txt -> +<tab>
string input = completionContext.RelatedAsts[0].Extent.Text;
if (Regex.IsMatch(input, @"^[\S]+$") && completionContext.RelatedAsts.Count > 0 && completionContext.RelatedAsts[0] is ScriptBlockAst)
{
replacementIndex = completionContext.RelatedAsts[0].Extent.StartScriptPosition.Offset;
replacementLength = completionContext.RelatedAsts[0].Extent.EndScriptPosition.Offset - replacementIndex;
completionContext.WordToComplete = input;
result = CompleteFileNameAsCommand(completionContext);
}
}
return result;
}
// Helper method to auto complete hashtable key
private static List<CompletionResult> GetResultForHashtable(CompletionContext completionContext)
{
var lastAst = completionContext.RelatedAsts.Last();
HashtableAst tempHashtableAst = null;
IScriptPosition cursor = completionContext.CursorPosition;
var hashTableAst = lastAst as HashtableAst;
if (hashTableAst != null)
{
// Check if the cursor within the hashtable
if (cursor.Offset < hashTableAst.Extent.EndOffset)
{
tempHashtableAst = hashTableAst;
}
else if (cursor.Offset == hashTableAst.Extent.EndOffset)
{
// Exclude the scenario that cursor at the end of hashtable, i.e. after '}'
if (completionContext.TokenAtCursor == null ||
completionContext.TokenAtCursor.Kind != TokenKind.RCurly)
{
tempHashtableAst = hashTableAst;
}
}
}
else
{
// Handle property completion on a blank line for DynamicKeyword statement
Ast lastChildofHashtableAst;
hashTableAst = Ast.GetAncestorHashtableAst(lastAst, out lastChildofHashtableAst);
// Check if the hashtable within a DynamicKeyword statement
if (hashTableAst != null)
{
var keywordAst = Ast.GetAncestorAst<DynamicKeywordStatementAst>(hashTableAst);
if (keywordAst != null)
{
// Handle only empty line
if (string.IsNullOrWhiteSpace(cursor.Line))
{
// Check if the cursor outside of last child of hashtable and within the hashtable
if (cursor.Offset > lastChildofHashtableAst.Extent.EndOffset &&
cursor.Offset <= hashTableAst.Extent.EndOffset)
{
tempHashtableAst = hashTableAst;
}
}
}
}
}
hashTableAst = tempHashtableAst;
if (hashTableAst != null)
{
completionContext.ReplacementIndex = completionContext.CursorPosition.Offset;
completionContext.ReplacementLength = 0;
return CompletionCompleters.CompleteHashtableKey(completionContext, hashTableAst);
}
return null;
}
// Helper method to look for an incomplete assignment pair in hash table.
private static bool CheckForPendingAssignment(HashtableAst hashTableAst)
{
foreach (var keyValue in hashTableAst.KeyValuePairs)
{
if (keyValue.Item2 is ErrorStatementAst)
{
// This indicates the assignment has not completed.
return true;
}
}
return false;
}
internal static TypeName FindTypeNameToComplete(ITypeName type, IScriptPosition cursor)
{
var typeName = type as TypeName;
if (typeName != null)
{
// If the cursor is at the start offset, it's not really inside, so return null.
// If the cursor is at the end offset, it's not really inside, but it's just before the cursor,
// we don want to complete it.
return (cursor.Offset > type.Extent.StartOffset && cursor.Offset <= type.Extent.EndOffset)
? typeName
: null;
}
var genericTypeName = type as GenericTypeName;
if (genericTypeName != null)
{
typeName = FindTypeNameToComplete(genericTypeName.TypeName, cursor);
if (typeName != null)
return typeName;
foreach (var t in genericTypeName.GenericArguments)
{
typeName = FindTypeNameToComplete(t, cursor);
if (typeName != null)
return typeName;
}
return null;
}
var arrayTypeName = type as ArrayTypeName;
if (arrayTypeName != null)
{
return FindTypeNameToComplete(arrayTypeName.ElementType, cursor) ?? null;
}
return null;
}
private static string GetFirstLineSubString(string stringToComplete, out bool hasNewLine)
{
hasNewLine = false;
if (!string.IsNullOrEmpty(stringToComplete))
{
var index = stringToComplete.IndexOfAny(Utils.Separators.CrLf);
if (index >= 0)
{
stringToComplete = stringToComplete.Substring(0, index);
hasNewLine = true;
}
}
return stringToComplete;
}
private static Tuple<ExpressionAst, StatementAst> GetHashEntryContainsCursor(
IScriptPosition cursor,
HashtableAst hashTableAst,
bool isCursorInString)
{
Tuple<ExpressionAst, StatementAst> keyValuePairWithCursor = null;
foreach (var kvp in hashTableAst.KeyValuePairs)
{
if (IsCursorWithinOrJustAfterExtent(cursor, kvp.Item2.Extent))
{
keyValuePairWithCursor = kvp;
break;
}
if (!isCursorInString)
{
//
// Handle following case, cursor is after '=' but before next key value pair,
// next key value pair will be treated as kvp.Item2 of 'Ensure' key
//
// configuration foo
// {
// File foo
// {
// DestinationPath = "\foo.txt"
// Ensure = |
// DependsOn =@("[User]x")
// }
// }
//
if (kvp.Item2.Extent.StartLineNumber > kvp.Item1.Extent.EndLineNumber &&
IsCursorAfterExtentAndInTheSameLine(cursor, kvp.Item1.Extent))
{
keyValuePairWithCursor = kvp;
break;
}
//
// If cursor is not within a string, then handle following two cases,
//
// #1) cursor is after '=', in the same line of previous key value pair
// configuration test{File testfile{DestinationPath='c:\test'; Ensure = |
//
// #2) cursor is after '=', in the separate line of previous key value pair
// configuration test{File testfile{DestinationPath='c:\test';
// Ensure = |
//
if (!IsCursorBeforeExtent(cursor, kvp.Item1.Extent) &&
IsCursorAfterExtentAndInTheSameLine(cursor, kvp.Item2.Extent))
{
keyValuePairWithCursor = kvp;
}
}
}
return keyValuePairWithCursor;
}
// Pulls the variable out of an assignment's LHS expression
// Also brings back the innermost type constraint if there is one
private static VariableExpressionAst GetVariableFromExpressionAst(
ExpressionAst expression,
ref Type typeConstraint,
ref ValidateSetAttribute setConstraint)
{
switch (expression)
{
// $x = ...
case VariableExpressionAst variableExpression:
return variableExpression;
// [type]$x = ...
case ConvertExpressionAst convertExpression:
typeConstraint = convertExpression.Type.TypeName.GetReflectionType();
return GetVariableFromExpressionAst(convertExpression.Child, ref typeConstraint, ref setConstraint);
// [attribute()][type]$x = ...
case AttributedExpressionAst attributedExpressionAst:
try
{
setConstraint = attributedExpressionAst.Attribute.GetAttribute() as ValidateSetAttribute;
}
catch
{
// Do nothing, just prevent fallout from an unsuccessful attribute conversion
}
return GetVariableFromExpressionAst(attributedExpressionAst.Child, ref typeConstraint, ref setConstraint);
// Something else, like `MemberExpressionAst` $a.p = <tab> which isn't currently handled
default:
return null;
}
}
// Gets any type constraints or validateset constraints on a given variable
private static bool TryGetTypeConstraintOnVariable(
CompletionContext completionContext,
string variableName,
out Type typeConstraint,
out ValidateSetAttribute setConstraint)
{
typeConstraint = null;
setConstraint = null;
PSVariable variable = completionContext.ExecutionContext.EngineSessionState.GetVariable(variableName);
if (variable == null || variable.Attributes.Count == 0)
{
return false;
}
foreach (Attribute attribute in variable.Attributes)
{
if (attribute is ArgumentTypeConverterAttribute typeConverterAttribute)
{
typeConstraint = typeConverterAttribute.TargetType;
continue;
}
if (attribute is ValidateSetAttribute validateSetAttribute)
{
setConstraint = validateSetAttribute;
}
}
return typeConstraint != null || setConstraint != null;
}
private static bool TryGetCompletionsForVariableAssignment(
CompletionContext completionContext,
AssignmentStatementAst assignmentAst,
out List<CompletionResult> completions)
{
bool TryGetResultForEnum(Type typeConstraint, CompletionContext completionContext, out List<CompletionResult> completions)
{
completions = null;
if (typeConstraint != null && typeConstraint.IsEnum)
{
completions = GetResultForEnum(typeConstraint, completionContext);
return true;
}
return false;
}
bool TryGetResultForSet(Type typeConstraint, ValidateSetAttribute setConstraint, CompletionContext completionContext1, out List<CompletionResult> completions)
{
completions = null;
if (setConstraint?.ValidValues != null)
{
completions = GetResultForSet(typeConstraint, setConstraint.ValidValues, completionContext);
return true;
}
return false;
}
completions = null;
// Try to get the variable from the assignment, plus any type constraint on it
Type typeConstraint = null;
ValidateSetAttribute setConstraint = null;
VariableExpressionAst variableAst = GetVariableFromExpressionAst(assignmentAst.Left, ref typeConstraint, ref setConstraint);
if (variableAst == null)
{
return false;
}
// Assignment constraints override any existing ones, so try them first
// Check any [ValidateSet()] constraint first since it's likely to be narrow
if (TryGetResultForSet(typeConstraint, setConstraint, completionContext, out completions))
{
return true;
}
// Then try to complete for an enum type
if (TryGetResultForEnum(typeConstraint, completionContext, out completions))
{
return true;
}
// If the assignment itself was unconstrained, the variable still might be
if (!TryGetTypeConstraintOnVariable(completionContext, variableAst.VariablePath.UserPath, out typeConstraint, out setConstraint))
{
return false;
}
// Again try the [ValidateSet()] constraint first
if (TryGetResultForSet(typeConstraint, setConstraint, completionContext, out completions))
{
return true;
}
// Then try to complete for an enum type again
if (TryGetResultForEnum(typeConstraint, completionContext, out completions))
{
return true;
}
return false;
}
private static List<CompletionResult> GetResultForSet(
Type typeConstraint,
IList<string> validValues,
CompletionContext completionContext)
{
var allValues = new List<string>();
foreach (string value in validValues)
{
if (typeConstraint != null && (typeConstraint == typeof(string) || typeConstraint.IsEnum))
{
allValues.Add(GetQuotedString(value, completionContext));
}
else
{
allValues.Add(value);
}
}
return GetMatchedResults(allValues, completionContext);
}
private static List<CompletionResult> GetMatchedResults(
List<string> allValues,
CompletionContext completionContext)
{
var stringToComplete = string.Empty;
if (completionContext.TokenAtCursor != null && completionContext.TokenAtCursor.Kind != TokenKind.Equals)
{
stringToComplete = completionContext.TokenAtCursor.Text;
}
IEnumerable<string> matchedResults = null;
if (!string.IsNullOrEmpty(stringToComplete))
{
string matchString = stringToComplete + "*";
var wildcardPattern = WildcardPattern.Get(matchString, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant);
matchedResults = allValues.Where(r => wildcardPattern.IsMatch(r));
}
else
{
matchedResults = allValues;
}
var result = new List<CompletionResult>();
foreach (var match in matchedResults)
{
result.Add(new CompletionResult(match));
}
return result;
}
private static string GetQuotedString(
string value,
CompletionContext completionContext)
{
var stringToComplete = string.Empty;
if (completionContext.TokenAtCursor != null)
{
stringToComplete = completionContext.TokenAtCursor.Text;
}
var quote = stringToComplete.StartsWith('"') ? "\"" : "'";
return quote + value + quote;
}
private static List<CompletionResult> GetResultForEnum(
Type type,
CompletionContext completionContext)
{
var allNames = new List<string>();
foreach (var name in Enum.GetNames(type))
{
allNames.Add(GetQuotedString(name, completionContext));
}
allNames.Sort();
return GetMatchedResults(allNames, completionContext);
}
private static List<CompletionResult> GetResultForEnumPropertyValueOfDSCResource(
CompletionContext completionContext,
string stringToComplete,
ref int replacementIndex,
ref int replacementLength,
out bool shouldContinue)
{
shouldContinue = true;
bool isCursorInString = completionContext.TokenAtCursor is StringToken;
List<CompletionResult> result = null;
var lastAst = completionContext.RelatedAsts.Last();
Ast lastChildofHashtableAst;
var hashTableAst = Ast.GetAncestorHashtableAst(lastAst, out lastChildofHashtableAst);
Diagnostics.Assert(stringToComplete != null, "stringToComplete should never be null");
// Check if the hashtable within a DynamicKeyword statement
if (hashTableAst != null)
{
var keywordAst = Ast.GetAncestorAst<DynamicKeywordStatementAst>(hashTableAst);
if (keywordAst != null)
{
IScriptPosition cursor = completionContext.CursorPosition;
var keyValuePairWithCursor = GetHashEntryContainsCursor(cursor, hashTableAst, isCursorInString);
if (keyValuePairWithCursor != null)
{
var propertyNameAst = keyValuePairWithCursor.Item1 as StringConstantExpressionAst;
if (propertyNameAst != null)
{
DynamicKeywordProperty property;
if (keywordAst.Keyword.Properties.TryGetValue(propertyNameAst.Value, out property))
{
List<string> 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<string>();
if (string.Equals(property.TypeConstraint, "StringArray", StringComparison.OrdinalIgnoreCase))
{
var arrayAst = Ast.GetAncestorAst<ArrayLiteralAst>(lastAst);
if (arrayAst != null && arrayAst.Elements.Count > 0)
{
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))
{
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<CompletionResult>();
}
Diagnostics.Assert(isCursorInString || (!hasNewLine), "hasNoQuote and hasNewLine cannot be true at the same time");
if (property.ValueMap != null && property.ValueMap.Count > 0)
{
IEnumerable<string> orderedValues = property.ValueMap.Keys.OrderBy(static 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)
{
string completionText = isCursorInString ? value : stringQuote + value + stringQuote;
if (hasNewLine)
completionText += stringQuote;
result.Add(new CompletionResult(
completionText,
value,
CompletionResultType.Text,
value));
}
}
else if (isDependsOnProperty)
{
var configAst = Ast.GetAncestorAst<ConfigurationDefinitionAst>(keywordAst);
if (configAst != null)
{
var namedBlockAst = Ast.GetAncestorAst<NamedBlockAst>(keywordAst);
if (namedBlockAst != null)
{
List<string> allResources = new List<string>();
foreach (var statementAst in namedBlockAst.Statements)
{
var dynamicKeywordAst = statementAst as DynamicKeywordStatementAst;
if (dynamicKeywordAst != null &&
dynamicKeywordAst != keywordAst &&
!string.Equals(dynamicKeywordAst.Keyword.Keyword, @"Node", StringComparison.OrdinalIgnoreCase))
{
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))
{
allResources.Add(resource);
}
}
}
}
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 += stringQuote;
result.Add(new CompletionResult(
completionText,
resource,
CompletionResultType.Text,
resource));
}
}
}
}
}
}
}
}
}
return result;
}
private List<CompletionResult> GetResultForString(CompletionContext completionContext, ref int replacementIndex, ref int replacementLength, bool isQuotedString)
{
// When it's the content of a quoted string, we only handle variable/member completion
if (isQuotedString) { return null; }
var tokenAtCursor = completionContext.TokenAtCursor;
var lastAst = completionContext.RelatedAsts.Last();
List<CompletionResult> 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;
string subInput = null;
bool shouldContinue;
result = GetResultForEnumPropertyValueOfDSCResource(completionContext, strValue, ref replacementIndex, ref replacementLength, out shouldContinue);
if (!shouldContinue || (result != null && result.Count > 0))
{
return result;
}
if (strType == StringConstantType.DoubleQuoted)
{
var match = Regex.Match(strValue, @"(\$[\w\d]+\.[\w\d\*]*)$");
if (match.Success)
{
subInput = match.Groups[1].Value;
}
else if ((match = Regex.Match(strValue, @"(\[[\w\d\.]+\]::[\w\d\*]*)$")).Success)
{
subInput = match.Groups[1].Value;
}
}
// Handle variable/member completion
if (subInput != null)
{
int stringStartIndex = tokenAtCursor.Extent.StartScriptPosition.Offset;
int cursorIndexInString = _cursorPosition.Offset - stringStartIndex - 1;
if (cursorIndexInString >= strValue.Length)
cursorIndexInString = strValue.Length;
var analysis = new CompletionAnalysis(_ast, _tokens, _cursorPosition, _options);
var subContext = analysis.CreateCompletionContext(completionContext.TypeInferenceContext);
var subResult = analysis.GetResultHelper(subContext, out int subReplaceIndex, out _, true);
if (subResult != null && subResult.Count > 0)
{
result = new List<CompletionResult>();
replacementIndex = stringStartIndex + 1 + (cursorIndexInString - subInput.Length);
replacementLength = subInput.Length;
ReadOnlySpan<char> prefix = subInput.AsSpan(0, subReplaceIndex);
foreach (CompletionResult entry in subResult)
{
string completionText = string.Concat(prefix, entry.CompletionText.AsSpan());
if (entry.ResultType == CompletionResultType.Property)
{
completionText = TokenKind.DollarParen.Text() + completionText + TokenKind.RParen.Text();
}
else if (entry.ResultType == CompletionResultType.Method)
{
completionText = TokenKind.DollarParen.Text() + completionText;
}
completionText += "\"";
result.Add(new CompletionResult(completionText, entry.ListItemText, entry.ResultType, entry.ToolTip));
}
}
}
else
{
var commandElementAst = lastAst as CommandElementAst;
string wordToComplete =
CompletionCompleters.ConcatenateStringPathArguments(commandElementAst, string.Empty, completionContext);
if (wordToComplete != null)
{
completionContext.WordToComplete = wordToComplete;
// Handle scenarios like this: cd 'c:\windows\win'<tab>
if (lastAst.Parent is CommandAst || lastAst.Parent is CommandParameterAst)
{
result = CompletionCompleters.CompleteCommandArgument(completionContext);
replacementIndex = completionContext.ReplacementIndex;
replacementLength = completionContext.ReplacementLength;
}
// Handle scenarios like this: "c:\wind"<tab>. Treat the StringLiteral/StringExpandable as path/command
else
{
// Handle path/commandname completion for quoted string
result = new List<CompletionResult>(CompletionCompleters.CompleteFilename(completionContext));
// Try command name completion only if the text contains '-'
if (wordToComplete.Contains('-'))
{
var commandNameResult = CompletionCompleters.CompleteCommand(completionContext);
if (commandNameResult != null && commandNameResult.Count > 0)
{
result.AddRange(commandNameResult);
}
}
}
}
}
return result;
}
/// <summary>
/// Find the configuration statement contains current cursor.
/// </summary>
/// <param name="cursorPosition"></param>
/// <param name="ast"></param>
/// <param name="keywordAst"></param>
/// <returns></returns>
private static ConfigurationDefinitionAst GetAncestorConfigurationAstAndKeywordAst(
IScriptPosition cursorPosition,
Ast ast,
out DynamicKeywordStatementAst keywordAst)
{
ConfigurationDefinitionAst configureAst = Ast.GetAncestorConfigurationDefinitionAstAndDynamicKeywordStatementAst(ast, out keywordAst);
// Find the configuration statement contains current cursor
// Note: cursorPosition.Offset < configureAst.Extent.EndOffset means cursor locates inside the configuration
// cursorPosition.Offset = configureAst.Extent.EndOffset means cursor locates at the end of the configuration
while (configureAst != null && cursorPosition.Offset > configureAst.Extent.EndOffset)
{
configureAst = Ast.GetAncestorAst<ConfigurationDefinitionAst>(configureAst.Parent);
}
return configureAst;
}
/// <summary>
/// Generate auto complete results for identifier within configuration.
/// Results are generated based on DynamicKeywords matches given identifier.
/// For example, following "Fi" matches "File", and "Us" matches "User"
///
/// Configuration
/// {
/// Fi^
/// Node("TargetMachine")
/// {
/// Us^
/// }
/// }
/// </summary>
/// <param name="completionContext"></param>
/// <param name="configureAst"></param>
/// <param name="keywordAst"></param>
/// <param name="matched"></param>
/// <returns></returns>
private static List<CompletionResult> GetResultForIdentifierInConfiguration(
CompletionContext completionContext,
ConfigurationDefinitionAst configureAst,
DynamicKeywordStatementAst keywordAst,
out bool matched)
{
List<CompletionResult> results = null;
matched = false;
IEnumerable<DynamicKeyword> keywords = configureAst.DefinedKeywords.Where(
k => // Node is special case, legal in both Resource and Meta configuration
string.Equals(k.Keyword, @"Node", StringComparison.OrdinalIgnoreCase) ||
(
// Check compatibility between Resource and Configuration Type
k.IsCompatibleWithConfigurationType(configureAst.ConfigurationType) &&
!DynamicKeyword.IsHiddenKeyword(k.Keyword) &&
!k.IsReservedKeyword
)
);
if (keywordAst != null && completionContext.CursorPosition.Offset < keywordAst.Extent.EndOffset)
keywords = keywordAst.Keyword.GetAllowedKeywords(keywords);
if (keywords != null && keywords.Any())
{
string commandName = (completionContext.WordToComplete ?? string.Empty) + "*";
var wildcardPattern = WildcardPattern.Get(commandName, WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant);
// Filter by name
var matchedResults = keywords.Where(k => wildcardPattern.IsMatch(k.Keyword));
if (matchedResults == null || !matchedResults.Any())
{
// Fallback to all legal keywords in the configuration statement
matchedResults = keywords;
}
else
{
matched = true;
}
foreach (var keyword in matchedResults)
{
string usageString = string.Empty;
ICrossPlatformDsc dscSubsystem = SubsystemManager.GetSubsystem<ICrossPlatformDsc>();
if (dscSubsystem != null)
{
usageString = dscSubsystem.GetDSCResourceUsageString(keyword);
}
else
{
usageString = Microsoft.PowerShell.DesiredStateConfiguration.Internal.DscClassCache.GetDSCResourceUsageString(keyword);
}
if (results == null)
{
results = new List<CompletionResult>();
}
results.Add(new CompletionResult(
keyword.Keyword,
keyword.Keyword,
CompletionResultType.DynamicKeyword,
usageString));
}
}
return results;
}
private List<CompletionResult> GetResultForIdentifier(CompletionContext completionContext, ref int replacementIndex, ref int replacementLength, bool isQuotedString)
{
var tokenAtCursor = completionContext.TokenAtCursor;
var lastAst = completionContext.RelatedAsts.Last();
List<CompletionResult> result = null;
var tokenAtCursorText = tokenAtCursor.Text;
completionContext.WordToComplete = tokenAtCursorText;
var strConst = lastAst as StringConstantExpressionAst;
if (strConst != null)
{
if (strConst.Value.Equals("$", StringComparison.Ordinal))
{
completionContext.WordToComplete = string.Empty;
return CompletionCompleters.CompleteVariable(completionContext);
}
else
{
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)
{
case UsingStatementKind.Assembly:
break;
case UsingStatementKind.Command:
break;
case UsingStatementKind.Module:
var moduleExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
StringLiterals.PowerShellModuleFileExtension,
StringLiterals.PowerShellDataFileExtension,
StringLiterals.PowerShellNgenAssemblyExtension,
StringLiterals.PowerShellILAssemblyExtension,
StringLiterals.PowerShellILExecutableExtension,
StringLiterals.PowerShellCmdletizationFileExtension
};
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");
}
}
}
}
if (completionContext.TokenAtCursor.TokenFlags == TokenFlags.MemberName)
{
result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);
if (result is not null)
{
return result;
}
}
if ((tokenAtCursor.TokenFlags & TokenFlags.CommandName) != 0)
{
// 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);
}
if (cursorAst != null &&
cursorAst.Extent.EndLineNumber == tokenAtCursor.Extent.StartLineNumber &&
cursorAst.Extent.EndColumnNumber == tokenAtCursor.Extent.StartColumnNumber)
{
if (tokenAtCursorText.IndexOfAny(Utils.Separators.Directory) == 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;
}
}
}
// When it's the content of a quoted string, we only handle variable/member completion
if (isQuotedString) { return result; }
// Handle the StringExpandableToken;
var strToken = tokenAtCursor as StringExpandableToken;
if (strToken != null && strToken.NestedTokens != null && strConst != null)
{
try
{
string expandedString = null;
var expandableStringAst = new ExpandableStringExpressionAst(strConst.Extent, strConst.Value, StringConstantType.BareWord);
if (CompletionCompleters.IsPathSafelyExpandable(expandableStringAst: expandableStringAst,
extraText: string.Empty,
executionContext: completionContext.ExecutionContext,
expandedString: out expandedString))
{
completionContext.WordToComplete = expandedString;
}
else
{
return result;
}
}
catch (Exception)
{
return result;
}
}
//
// Handle completion of DSC resources within Configuration
//
DynamicKeywordStatementAst keywordAst;
ConfigurationDefinitionAst configureAst = GetAncestorConfigurationAstAndKeywordAst(completionContext.CursorPosition, lastAst, out keywordAst);
bool matched = false;
List<CompletionResult> keywordResult = null;
if (configureAst != null)
{
// Current token is within ConfigurationDefinitionAst or DynamicKeywordStatementAst
keywordResult = GetResultForIdentifierInConfiguration(completionContext, configureAst, keywordAst, out matched);
}
// Handle the file completion before command name completion
result = CompleteFileNameAsCommand(completionContext);
// Handle the command name completion
var commandNameResult = CompletionCompleters.CompleteCommand(completionContext);
if (commandNameResult != null && commandNameResult.Count > 0)
{
result.AddRange(commandNameResult);
}
if (matched && keywordResult != null)
{
result.InsertRange(0, keywordResult);
}
else if (!matched && keywordResult != null && commandNameResult.Count == 0)
{
result.AddRange(keywordResult);
}
return result;
}
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 (isSingleDash)
{
if (isQuotedString) { return result; }
var res = CompletionCompleters.CompleteCommandParameter(completionContext);
if (res.Count != 0)
{
return res;
}
}
return CompletionCompleters.CompleteCommandArgument(completionContext);
}
TokenKind memberOperator = TokenKind.Unknown;
bool isMemberCompletion = (lastAst.Parent is MemberExpressionAst);
bool isStatic = isMemberCompletion && ((MemberExpressionAst)lastAst.Parent).Static;
bool isWildcard = false;
if (!isMemberCompletion)
{
// Still might be member completion, something like: echo $member.
// 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 (tokenAtCursorText.Equals(TokenKind.Dot.Text(), StringComparison.Ordinal))
{
memberOperator = TokenKind.Dot;
isMemberCompletion = true;
}
else if (tokenAtCursorText.Equals(TokenKind.ColonColon.Text(), StringComparison.Ordinal))
{
memberOperator = TokenKind.ColonColon;
isMemberCompletion = true;
}
else if (tokenAtCursor.Kind.Equals(TokenKind.Multiply) && lastAst is BinaryExpressionAst)
{
// Handle member completion with wildcard(wildcard is at the end): $a.p*
var binaryExpressionAst = (BinaryExpressionAst)lastAst;
var memberExpressionAst = binaryExpressionAst.Left as MemberExpressionAst;
var errorPosition = binaryExpressionAst.ErrorPosition;
if (memberExpressionAst != null && binaryExpressionAst.Operator == TokenKind.Multiply &&
errorPosition.StartOffset == memberExpressionAst.Member.Extent.EndOffset)
{
isStatic = memberExpressionAst.Static;
memberOperator = isStatic ? TokenKind.ColonColon : TokenKind.Dot;
isMemberCompletion = true;
isWildcard = true;
// Member completion will add back the '*', so pretend it wasn't there, at least from the "related asts" point of view,
// but add the member expression that we are really completing.
completionContext.RelatedAsts.Remove(binaryExpressionAst);
completionContext.RelatedAsts.Add(memberExpressionAst);
var memberAst = memberExpressionAst.Member as StringConstantExpressionAst;
if (memberAst != null)
{
replacementIndex = memberAst.Extent.StartScriptPosition.Offset;
replacementLength += memberAst.Extent.Text.Length;
}
}
}
}
if (isMemberCompletion)
{
result = CompletionCompleters.CompleteMember(completionContext, @static: (isStatic || memberOperator == TokenKind.ColonColon));
// If the last token was just a '.', we tried to complete members. That may
// have failed because it wasn't really an attempt to complete a member, in
// which case we should try to complete as an argument.
if (result.Count > 0)
{
if (!isWildcard && memberOperator != TokenKind.Unknown)
{
replacementIndex += tokenAtCursorText.Length;
replacementLength = 0;
}
return result;
}
}
if (lastAst.Parent is HashtableAst)
{
result = CompletionCompleters.CompleteHashtableKey(completionContext, (HashtableAst)lastAst.Parent);
if (result != null && result.Count > 0)
{
return result;
}
}
// When it's the content of a quoted string, we only handle variable/member completion
if (isQuotedString) { return result; }
bool needFileCompletion = false;
if (lastAst.Parent is FileRedirectionAst || CompleteAgainstSwitchFile(lastAst, completionContext.TokenBeforeCursor))
{
string wordToComplete =
CompletionCompleters.ConcatenateStringPathArguments(lastAst as CommandElementAst, string.Empty, completionContext);
if (wordToComplete != null)
{
needFileCompletion = true;
completionContext.WordToComplete = wordToComplete;
}
}
else if (tokenAtCursorText.IndexOfAny(Utils.Separators.Directory) == 0)
{
var command = lastAst.Parent as CommandBaseAst;
if (command != null && command.Redirections.Count > 0)
{
var fileRedirection = command.Redirections[0] as FileRedirectionAst;
if (fileRedirection != null &&
fileRedirection.Extent.EndLineNumber == lastAst.Extent.StartLineNumber &&
fileRedirection.Extent.EndColumnNumber == lastAst.Extent.StartColumnNumber)
{
string wordToComplete =
CompletionCompleters.ConcatenateStringPathArguments(fileRedirection.Location, tokenAtCursorText, completionContext);
if (wordToComplete != null)
{
needFileCompletion = true;
completionContext.WordToComplete = wordToComplete;
replacementIndex = fileRedirection.Location.Extent.StartScriptPosition.Offset;
replacementLength += fileRedirection.Location.Extent.EndScriptPosition.Offset - replacementIndex;
completionContext.ReplacementIndex = replacementIndex;
completionContext.ReplacementLength = replacementLength;
}
}
}
}
if (needFileCompletion)
{
return new List<CompletionResult>(CompletionCompleters.CompleteFilename(completionContext));
}
else
{
string wordToComplete =
CompletionCompleters.ConcatenateStringPathArguments(lastAst as CommandElementAst, string.Empty, completionContext);
if (wordToComplete != null)
{
completionContext.WordToComplete = wordToComplete;
}
}
result = CompletionCompleters.CompleteCommandArgument(completionContext);
replacementIndex = completionContext.ReplacementIndex;
replacementLength = completionContext.ReplacementLength;
return result;
}
private static List<CompletionResult> GetResultForAttributeArgument(CompletionContext completionContext, ref int replacementIndex, ref int replacementLength)
{
// Attribute member arguments
Type attributeType = null;
string argName = string.Empty;
Ast argAst = completionContext.RelatedAsts.Find(static ast => ast is NamedAttributeArgumentAst);
NamedAttributeArgumentAst namedArgAst = argAst as NamedAttributeArgumentAst;
if (argAst != null && namedArgAst != null)
{
attributeType = ((AttributeAst)namedArgAst.Parent).TypeName.GetReflectionAttributeType();
argName = namedArgAst.ArgumentName;
replacementIndex = namedArgAst.Extent.StartOffset;
replacementLength = argName.Length;
}
else
{
Ast astAtt = completionContext.RelatedAsts.Find(static ast => ast is AttributeAst);
AttributeAst attAst = astAtt as AttributeAst;
if (astAtt != null && attAst != null)
{
attributeType = attAst.TypeName.GetReflectionAttributeType();
}
}
if (attributeType != null)
{
PropertyInfo[] propertyInfos = attributeType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
List<CompletionResult> result = new List<CompletionResult>();
foreach (PropertyInfo property in propertyInfos)
{
// Ignore getter-only properties, including 'TypeId' (all attributes inherit it).
if (!property.CanWrite) { continue; }
if (property.Name.StartsWith(argName, StringComparison.OrdinalIgnoreCase))
{
result.Add(new CompletionResult(property.Name, property.Name, CompletionResultType.Property,
property.PropertyType.ToString() + " " + property.Name));
}
}
return result;
}
return null;
}
/// <summary>
/// Complete file name as command.
/// </summary>
/// <param name="completionContext"></param>
/// <returns></returns>
private static List<CompletionResult> CompleteFileNameAsCommand(CompletionContext completionContext)
{
var addAmpersandIfNecessary = CompletionCompleters.IsAmpersandNeeded(completionContext, true);
var result = new List<CompletionResult>();
var clearLiteralPathsKey = false;
if (completionContext.Options == null)
{
completionContext.Options = new Hashtable { { "LiteralPaths", true } };
}
else if (!completionContext.Options.ContainsKey("LiteralPaths"))
{
// Dont escape '[',']','`' when the file name is treated as command name
completionContext.Options.Add("LiteralPaths", true);
clearLiteralPathsKey = true;
}
try
{
var fileNameResult = CompletionCompleters.CompleteFilename(completionContext);
foreach (var entry in fileNameResult)
{
// Add '&' to file names that are quoted
var completionText = entry.CompletionText;
var len = completionText.Length;
if (addAmpersandIfNecessary && len > 2 && completionText[0].IsSingleQuote() && completionText[len - 1].IsSingleQuote())
{
completionText = "& " + completionText;
result.Add(new CompletionResult(completionText, entry.ListItemText, entry.ResultType, entry.ToolTip));
}
else
{
result.Add(entry);
}
}
}
finally
{
if (clearLiteralPathsKey)
completionContext.Options.Remove("LiteralPaths");
}
return result;
}
}
}