MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System;5using System.Collections;6using System.Collections.Generic;7using System.Linq;8using System.Management.Automation.Language;9using System.Reflection;10using System.Text;11using System.Text.RegularExpressions;12using System.Management.Automation.Subsystem;13using System.Management.Automation.Subsystem.DSC;14 15namespace System.Management.Automation16{17 internal class CompletionContext18 {19 internal List<Ast> RelatedAsts { get; set; }20 21 // Only one of TokenAtCursor or TokenBeforeCursor is set22 // This is how we can tell if we're trying to complete part of something (like a member)23 // or complete an argument, where TokenBeforeCursor could be a parameter name.24 internal Token TokenAtCursor { get; set; }25 26 internal Token TokenBeforeCursor { get; set; }27 28 internal IScriptPosition CursorPosition { get; set; }29 30 internal PowerShellExecutionHelper Helper { get; set; }31 32 internal Hashtable Options { get; set; }33 34 internal Dictionary<string, ScriptBlock> CustomArgumentCompleters { get; set; }35 36 internal Dictionary<string, ScriptBlock> NativeArgumentCompleters { get; set; }37 38 internal string WordToComplete { get; set; }39 40 internal int ReplacementIndex { get; set; }41 42 internal int ReplacementLength { get; set; }43 44 internal ExecutionContext ExecutionContext { get; set; }45 46 internal PseudoBindingInfo PseudoBindingInfo { get; set; }47 48 internal TypeInferenceContext TypeInferenceContext { get; set; }49 50 internal bool GetOption(string option, bool @default)51 {52 if (Options == null || !Options.ContainsKey(option))53 {54 return @default;55 }56 57 return LanguagePrimitives.ConvertTo<bool>(Options[option]);58 }59 }60 61 internal class CompletionAnalysis62 {63 private readonly Ast _ast;64 private readonly Token[] _tokens;65 private readonly IScriptPosition _cursorPosition;66 private readonly Hashtable _options;67 68 internal CompletionAnalysis(Ast ast, Token[] tokens, IScriptPosition cursorPosition, Hashtable options)69 {70 _ast = ast;71 _tokens = tokens;72 _cursorPosition = cursorPosition;73 _options = options;74 }75 76 private static bool IsInterestingToken(Token token)77 {78 return token.Kind != TokenKind.NewLine && token.Kind != TokenKind.EndOfInput;79 }80 81 private static bool IsCursorWithinOrJustAfterExtent(IScriptPosition cursor, IScriptExtent extent)82 {83 return cursor.Offset > extent.StartOffset && cursor.Offset <= extent.EndOffset;84 }85 86 private static bool IsCursorRightAfterExtent(IScriptPosition cursor, IScriptExtent extent)87 {88 return cursor.Offset == extent.EndOffset;89 }90 91 private static bool IsCursorAfterExtentAndInTheSameLine(IScriptPosition cursor, IScriptExtent extent)92 {93 return cursor.Offset >= extent.EndOffset && extent.EndLineNumber == cursor.LineNumber;94 }95 96 private static bool IsCursorBeforeExtent(IScriptPosition cursor, IScriptExtent extent)97 {98 return cursor.Offset < extent.StartOffset;99 }100 101 private static bool IsCursorAfterExtent(IScriptPosition cursor, IScriptExtent extent)102 {103 return extent.EndOffset < cursor.Offset;104 }105 106 private static bool IsCursorOutsideOfExtent(IScriptPosition cursor, IScriptExtent extent)107 {108 return cursor.Offset < extent.StartOffset || cursor.Offset > extent.EndOffset;109 }110 111 internal readonly struct AstAnalysisContext112 {113 internal AstAnalysisContext(Token tokenAtCursor, Token tokenBeforeCursor, List<Ast> relatedAsts, int replacementIndex)114 {115 TokenAtCursor = tokenAtCursor;116 TokenBeforeCursor = tokenBeforeCursor;117 RelatedAsts = relatedAsts;118 ReplacementIndex = replacementIndex;119 }120 121 internal readonly Token TokenAtCursor;122 internal readonly Token TokenBeforeCursor;123 internal readonly List<Ast> RelatedAsts;124 internal readonly int ReplacementIndex;125 }126 127 internal static AstAnalysisContext ExtractAstContext(Ast inputAst, Token[] inputTokens, IScriptPosition cursor)128 {129 bool adjustLineAndColumn = false;130 IScriptPosition positionForAstSearch = cursor;131 132 Token tokenBeforeCursor = null;133 Token tokenAtCursor = InterstingTokenAtCursorOrDefault(inputTokens, cursor);134 if (tokenAtCursor == null)135 {136 tokenBeforeCursor = InterstingTokenBeforeCursorOrDefault(inputTokens, cursor);137 if (tokenBeforeCursor != null)138 {139 positionForAstSearch = tokenBeforeCursor.Extent.EndScriptPosition;140 adjustLineAndColumn = true;141 }142 }143 else144 {145 var stringExpandableToken = tokenAtCursor as StringExpandableToken;146 if (stringExpandableToken?.NestedTokens != null)147 {148 tokenAtCursor = InterstingTokenAtCursorOrDefault(stringExpandableToken.NestedTokens, cursor) ?? stringExpandableToken;149 }150 }151 152 int replacementIndex = adjustLineAndColumn ? cursor.Offset : 0;153 List<Ast> relatedAsts = AstSearcher.FindAll(154 inputAst,155 ast => IsCursorWithinOrJustAfterExtent(positionForAstSearch, ast.Extent),156 searchNestedScriptBlocks: true).ToList();157 158 if (relatedAsts.Count == 0)159 {160 relatedAsts.Add(inputAst);161 }162 163 // If the last ast is an unnamed block that starts with "param" the cursor is inside a param block.164 // To avoid adding special handling to all the completers that look at the last ast, we remove it here because it's not useful for completion.165 if (relatedAsts[^1].Extent.Text.StartsWith("param", StringComparison.OrdinalIgnoreCase)166 && relatedAsts[^1] is NamedBlockAst namedBlock && namedBlock.Unnamed)167 {168 relatedAsts.RemoveAt(relatedAsts.Count - 1);169 }170 171 Diagnostics.Assert(tokenAtCursor == null || tokenBeforeCursor == null, "Only one of these tokens can be non-null");172 173 return new AstAnalysisContext(tokenAtCursor, tokenBeforeCursor, relatedAsts, replacementIndex);174 }175 176 internal CompletionContext CreateCompletionContext(PowerShell powerShell)177 {178 var typeInferenceContext = new TypeInferenceContext(powerShell);179 return InitializeCompletionContext(typeInferenceContext);180 }181 182 internal CompletionContext CreateCompletionContext(TypeInferenceContext typeInferenceContext)183 {184 return InitializeCompletionContext(typeInferenceContext);185 }186 187 private CompletionContext InitializeCompletionContext(TypeInferenceContext typeInferenceContext)188 {189 var astContext = ExtractAstContext(_ast, _tokens, _cursorPosition);190 191 typeInferenceContext.CurrentTypeDefinitionAst ??= Ast.GetAncestorTypeDefinitionAst(astContext.RelatedAsts.Last());192 193 ExecutionContext executionContext = typeInferenceContext.ExecutionContext;194 195 return new CompletionContext196 {197 Options = _options,198 CursorPosition = _cursorPosition,199 TokenAtCursor = astContext.TokenAtCursor,200 TokenBeforeCursor = astContext.TokenBeforeCursor,201 RelatedAsts = astContext.RelatedAsts,202 ReplacementIndex = astContext.ReplacementIndex,203 ExecutionContext = executionContext,204 TypeInferenceContext = typeInferenceContext,205 Helper = typeInferenceContext.Helper,206 CustomArgumentCompleters = executionContext.CustomArgumentCompleters,207 NativeArgumentCompleters = executionContext.NativeArgumentCompleters,208 };209 }210 211 private static Token InterstingTokenAtCursorOrDefault(IReadOnlyList<Token> tokens, IScriptPosition cursorPosition)212 {213 for (int i = tokens.Count - 1; i >= 0; --i)214 {215 Token token = tokens[i];216 if (IsCursorWithinOrJustAfterExtent(cursorPosition, token.Extent) && IsInterestingToken(token))217 {218 return token;219 }220 }221 222 return null;223 }224 225 private static Token InterstingTokenBeforeCursorOrDefault(IReadOnlyList<Token> tokens, IScriptPosition cursorPosition)226 {227 for (int i = tokens.Count - 1; i >= 0; --i)228 {229 Token token = tokens[i];230 if (IsCursorAfterExtent(cursorPosition, token.Extent) && IsInterestingToken(token))231 {232 return token;233 }234 }235 236 return null;237 }238 239 private static Ast GetLastAstAtCursor(ScriptBlockAst scriptBlockAst, IScriptPosition cursorPosition)240 {241 var asts = AstSearcher.FindAll(scriptBlockAst, ast => IsCursorRightAfterExtent(cursorPosition, ast.Extent), searchNestedScriptBlocks: true);242 return asts.LastOrDefault();243 }244 245 #region Special Cases246 247 /// <summary>248 /// Check if we should complete file names for "switch -file"249 /// </summary>250 private static bool CompleteAgainstSwitchFile(Ast lastAst, Token tokenBeforeCursor)251 {252 Tuple<Token, Ast> fileConditionTuple;253 254 if (lastAst is ErrorStatementAst errorStatement && errorStatement.Flags is not null && errorStatement.Kind is not null && tokenBeforeCursor is not null &&255 errorStatement.Kind.Kind.Equals(TokenKind.Switch) && errorStatement.Flags.TryGetValue("file", out fileConditionTuple))256 {257 // Handle "switch -file <tab>"258 return fileConditionTuple.Item1.Extent.EndOffset == tokenBeforeCursor.Extent.EndOffset;259 }260 261 if (lastAst.Parent is CommandExpressionAst)262 {263 // Handle "switch -file m<tab>" or "switch -file *.ps1<tab>"264 if (lastAst.Parent.Parent is not PipelineAst pipeline)265 {266 return false;267 }268 269 if (pipeline.Parent is not ErrorStatementAst parentErrorStatement || parentErrorStatement.Kind is null || parentErrorStatement.Flags is null)270 {271 return false;272 }273 274 return (parentErrorStatement.Kind.Kind.Equals(TokenKind.Switch) &&275 parentErrorStatement.Flags.TryGetValue("file", out fileConditionTuple) && fileConditionTuple.Item2 == pipeline);276 }277 278 return false;279 }280 281 /// <summary>282 /// Check if we should complete parameter names for switch cases on $PSBoundParameters.Keys283 /// </summary>284 private static List<CompletionResult> CompleteAgainstSwitchCaseCondition(CompletionContext completionContext)285 {286 var lastAst = completionContext.RelatedAsts.Last();287 288 PipelineAst conditionPipeline = null;289 Ast switchAst = null;290 291 // Check if we're in a switch statement (complete) or error statement (incomplete switch)292 if (lastAst.Parent is SwitchStatementAst switchStatementAst)293 {294 // Verify that the lastAst is one of the clause conditions (not in the body)295 bool isClauseCondition = switchStatementAst.Clauses.Any(clause => clause.Item1 == lastAst);296 297 if (!isClauseCondition)298 {299 return null;300 }301 302 conditionPipeline = switchStatementAst.Condition as PipelineAst;303 switchAst = switchStatementAst;304 }305 else306 {307 // Check for incomplete switch parsed as ErrorStatementAst308 if (lastAst.Parent is not ErrorStatementAst errorStatementAst || errorStatementAst.Kind is null ||309 errorStatementAst.Kind.Kind != TokenKind.Switch)310 {311 return null;312 }313 314 // For ErrorStatementAst, the case value is in Bodies, condition is in Conditions315 bool isInBodies = errorStatementAst.Bodies != null && errorStatementAst.Bodies.Any(body => body == lastAst);316 317 if (!isInBodies)318 {319 return null;320 }321 322 // Get the condition from ErrorStatementAst.Conditions323 if (errorStatementAst.Conditions != null && errorStatementAst.Conditions.Count > 0)324 {325 conditionPipeline = errorStatementAst.Conditions[0] as PipelineAst;326 }327 switchAst = errorStatementAst;328 }329 330 if (conditionPipeline == null || conditionPipeline.PipelineElements.Count != 1)331 {332 return null;333 }334 335 if (conditionPipeline.PipelineElements[0] is not CommandExpressionAst commandExpressionAst)336 {337 return null;338 }339 340 // Check if the expression is a member access on $PSBoundParameters.Keys341 if (commandExpressionAst.Expression is not MemberExpressionAst memberExpressionAst)342 {343 return null;344 }345 346 // Check if the target is $PSBoundParameters347 if (memberExpressionAst.Expression is not VariableExpressionAst variableExpressionAst ||348 !variableExpressionAst.VariablePath.UserPath.Equals("PSBoundParameters", StringComparison.OrdinalIgnoreCase))349 {350 return null;351 }352 353 // Check if the member is "Keys"354 if (memberExpressionAst.Member is not StringConstantExpressionAst memberNameAst ||355 !memberNameAst.Value.Equals("Keys", StringComparison.OrdinalIgnoreCase))356 {357 return null;358 }359 360 // Find the nearest param block by traversing up the AST361 var paramBlockAst = FindNearestParamBlock(switchAst.Parent);362 363 if (paramBlockAst == null || paramBlockAst.Parameters.Count == 0)364 {365 return null;366 }367 368 // Generate completion results from parameter names369 var wordToComplete = completionContext.WordToComplete ?? string.Empty;370 return CreateParameterCompletionResults(paramBlockAst, wordToComplete);371 }372 373 /// <summary>374 /// Check if we should complete parameter names for $PSBoundParameters access patterns375 /// Supports: $PSBoundParameters.ContainsKey('...'), $PSBoundParameters['...'], $PSBoundParameters.Remove('...')376 /// </summary>377 private static List<CompletionResult> CompleteAgainstPSBoundParametersAccess(CompletionContext completionContext)378 {379 var lastAst = completionContext.RelatedAsts.Last();380 381 // Must be a string constant382 if (lastAst is not StringConstantExpressionAst stringAst)383 {384 return null;385 }386 387 ExpressionAst targetAst = null;388 389 // Check for method invocation: $PSBoundParameters.ContainsKey('...') or $PSBoundParameters.Remove('...')390 if (lastAst.Parent is InvokeMemberExpressionAst invokeMemberAst)391 {392 if (invokeMemberAst.Member is StringConstantExpressionAst memberName &&393 (memberName.Value.Equals("ContainsKey", StringComparison.OrdinalIgnoreCase) ||394 memberName.Value.Equals("Remove", StringComparison.OrdinalIgnoreCase)))395 {396 targetAst = invokeMemberAst.Expression;397 }398 }399 // Check for indexer: $PSBoundParameters['...']400 else if (lastAst.Parent is IndexExpressionAst indexAst)401 {402 targetAst = indexAst.Target;403 }404 405 if (targetAst is null)406 {407 return null;408 }409 410 // Check if target is $PSBoundParameters411 if (targetAst is not VariableExpressionAst variableAst ||412 !variableAst.VariablePath.UserPath.Equals("PSBoundParameters", StringComparison.OrdinalIgnoreCase))413 {414 return null;415 }416 417 // Find the nearest param block418 var paramBlockAst = FindNearestParamBlock(lastAst.Parent);419 420 if (paramBlockAst == null || paramBlockAst.Parameters.Count == 0)421 {422 return null;423 }424 425 // Generate completion results from parameter names426 var wordToComplete = completionContext.WordToComplete ?? string.Empty;427 428 // Determine quote style based on the string constant type429 string quoteChar = string.Empty;430 if (stringAst.StringConstantType == StringConstantType.SingleQuoted)431 {432 quoteChar = "'";433 }434 else if (stringAst.StringConstantType == StringConstantType.DoubleQuoted)435 {436 quoteChar = "\"";437 }438 439 return CreateParameterCompletionResults(paramBlockAst, wordToComplete, quoteChar);440 }441 442 /// <summary>443 /// Finds the nearest ParamBlockAst by traversing up the AST hierarchy.444 /// </summary>445 /// <param name="startAst">The AST node to start searching from.</param>446 /// <returns>The nearest ParamBlockAst if found; otherwise, null.</returns>447 private static ParamBlockAst FindNearestParamBlock(Ast startAst)448 {449 Ast current = startAst;450 while (current != null)451 {452 if (current is FunctionDefinitionAst functionDefinitionAst)453 {454 return functionDefinitionAst.Body?.ParamBlock;455 }456 else if (current is ScriptBlockAst scriptBlockAst)457 {458 var paramBlock = scriptBlockAst.ParamBlock;459 if (paramBlock != null)460 {461 return paramBlock;462 }463 }464 465 current = current.Parent;466 }467 468 return null;469 }470 471 /// <summary>472 /// Creates completion results from parameter names with optional quote wrapping.473 /// </summary>474 /// <param name="paramBlockAst">The parameter block containing parameters to complete.</param>475 /// <param name="wordToComplete">The partial word to match against parameter names.</param>476 /// <param name="quoteChar">Optional quote character to wrap completion text (empty string for no quotes).</param>477 /// <returns>A list of completion results, or null if no matches found.</returns>478 private static List<CompletionResult> CreateParameterCompletionResults(479 ParamBlockAst paramBlockAst,480 string wordToComplete,481 string quoteChar = "")482 {483 var result = paramBlockAst.Parameters484 .Select(parameter => parameter.Name.VariablePath.UserPath)485 .Where(parameterName => parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase))486 .Select(parameterName =>487 new CompletionResult(488 quoteChar + parameterName + quoteChar,489 parameterName,490 CompletionResultType.ParameterValue,491 parameterName))492 .ToList();493 494 return result.Count > 0 ? result : null;495 }496 497 private static bool CompleteOperator(Token tokenAtCursor, Ast lastAst)498 {499 if (tokenAtCursor.Kind == TokenKind.Minus)500 {501 return lastAst is BinaryExpressionAst;502 }503 else if (tokenAtCursor.Kind == TokenKind.Parameter)504 {505 if (lastAst is CommandParameterAst)506 return lastAst.Parent is ExpressionAst;507 }508 509 return false;510 }511 512 private static bool CompleteAgainstStatementFlags(Ast scriptAst, Ast lastAst, Token token, out TokenKind kind)513 {514 kind = TokenKind.Unknown;515 516 // Handle "switch -f<tab>"517 var errorStatement = lastAst as ErrorStatementAst;518 if (errorStatement != null && errorStatement.Kind != null)519 {520 switch (errorStatement.Kind.Kind)521 {522 case TokenKind.Switch:523 kind = TokenKind.Switch;524 return true;525 default:526 break;527 }528 }529 530 // Handle "switch -<tab>". Skip cases like "switch ($a) {} -<tab> "531 var scriptBlockAst = scriptAst as ScriptBlockAst;532 if (token != null && token.Kind == TokenKind.Minus && scriptBlockAst != null)533 {534 var asts = AstSearcher.FindAll(scriptBlockAst, ast => IsCursorAfterExtent(token.Extent.StartScriptPosition, ast.Extent), searchNestedScriptBlocks: true);535 536 Ast last = asts.LastOrDefault();537 errorStatement = null;538 539 while (last != null)540 {541 errorStatement = last as ErrorStatementAst;542 if (errorStatement != null) { break; }543 544 last = last.Parent;545 }546 547 if (errorStatement != null && errorStatement.Kind != null)548 {549 switch (errorStatement.Kind.Kind)550 {551 case TokenKind.Switch:552 553 Tuple<Token, Ast> value;554 if (errorStatement.Flags != null && errorStatement.Flags.TryGetValue(Parser.VERBATIM_ARGUMENT, out value))555 {556 if (IsTokenTheSame(value.Item1, token))557 {558 kind = TokenKind.Switch;559 return true;560 }561 }562 563 break;564 565 default:566 break;567 }568 }569 }570 571 return false;572 }573 574 private static bool IsTokenTheSame(Token x, Token y)575 {576 if (x.Kind == y.Kind && x.TokenFlags == y.TokenFlags &&577 x.Extent.StartLineNumber == y.Extent.StartLineNumber &&578 x.Extent.StartColumnNumber == y.Extent.StartColumnNumber &&579 x.Extent.EndLineNumber == y.Extent.EndLineNumber &&580 x.Extent.EndColumnNumber == y.Extent.EndColumnNumber)581 {582 return true;583 }584 585 return false;586 }587 588 #endregion Special Cases589 590 internal List<CompletionResult> GetResults(PowerShell powerShell, out int replacementIndex, out int replacementLength)591 {592 var completionContext = CreateCompletionContext(powerShell);593 594 PSLanguageMode? previousLanguageMode = null;595 try596 {597 // Tab expansion is called from a trusted function - we should apply ConstrainedLanguage if necessary.598 if (completionContext.ExecutionContext.HasRunspaceEverUsedConstrainedLanguageMode)599 {600 previousLanguageMode = completionContext.ExecutionContext.LanguageMode;601 completionContext.ExecutionContext.LanguageMode = PSLanguageMode.ConstrainedLanguage;602 }603 604 return GetResultHelper(completionContext, out replacementIndex, out replacementLength);605 }606 finally607 {608 if (previousLanguageMode.HasValue)609 {610 completionContext.ExecutionContext.LanguageMode = previousLanguageMode.Value;611 }612 }613 }614 615 internal List<CompletionResult> GetResultHelper(CompletionContext completionContext, out int replacementIndex, out int replacementLength)616 {617 replacementIndex = -1;618 replacementLength = -1;619 620 var tokenAtCursor = completionContext.TokenAtCursor;621 var lastAst = completionContext.RelatedAsts.Last();622 List<CompletionResult> result = null;623 if (tokenAtCursor != null)624 {625 replacementIndex = tokenAtCursor.Extent.StartScriptPosition.Offset;626 replacementLength = tokenAtCursor.Extent.EndScriptPosition.Offset - replacementIndex;627 628 completionContext.ReplacementIndex = replacementIndex;629 completionContext.ReplacementLength = replacementLength;630 631 switch (tokenAtCursor.Kind)632 {633 case TokenKind.Variable:634 case TokenKind.SplattedVariable:635 completionContext.WordToComplete = ((VariableToken)tokenAtCursor).VariablePath.UserPath;636 result = CompletionCompleters.CompleteVariable(completionContext);637 break;638 639 case TokenKind.Multiply:640 case TokenKind.Generic:641 case TokenKind.MinusMinus: // for native commands '--'642 case TokenKind.Identifier:643 if (!tokenAtCursor.TokenFlags.HasFlag(TokenFlags.TypeName))644 {645 result = CompleteUsingKeywords(completionContext.CursorPosition.Offset, _tokens, ref replacementIndex, ref replacementLength);646 if (result is not null)647 {648 return result;649 }650 651 result = GetResultForIdentifier(completionContext, ref replacementIndex, ref replacementLength);652 }653 654 break;655 656 case TokenKind.Parameter:657 completionContext.WordToComplete = tokenAtCursor.Text;658 var cmdAst = lastAst.Parent as CommandAst;659 if (lastAst is StringConstantExpressionAst && cmdAst != null && cmdAst.CommandElements.Count == 1)660 {661 result = CompleteFileNameAsCommand(completionContext);662 break;663 }664 665 TokenKind statementKind;666 if (CompleteAgainstStatementFlags(null, lastAst, null, out statementKind))667 {668 result = CompletionCompleters.CompleteStatementFlags(statementKind, completionContext.WordToComplete);669 break;670 }671 672 if (CompleteOperator(tokenAtCursor, lastAst))673 {674 result = CompletionCompleters.CompleteOperator(completionContext.WordToComplete);675 break;676 }677 678 // Handle scenarios like this: dir -path:<tab>679 if (completionContext.WordToComplete.EndsWith(':'))680 {681 replacementIndex = tokenAtCursor.Extent.EndScriptPosition.Offset;682 replacementLength = 0;683 684 completionContext.WordToComplete = string.Empty;685 result = CompletionCompleters.CompleteCommandArgument(completionContext);686 }687 else688 {689 result = CompletionCompleters.CompleteCommandParameter(completionContext);690 }691 692 break;693 694 case TokenKind.Dot:695 case TokenKind.ColonColon:696 case TokenKind.QuestionDot:697 replacementIndex += tokenAtCursor.Text.Length;698 replacementLength = 0;699 result = CompletionCompleters.CompleteMember(completionContext, @static: tokenAtCursor.Kind == TokenKind.ColonColon, ref replacementLength);700 701 break;702 703 case TokenKind.Comment:704 completionContext.WordToComplete = tokenAtCursor.Text;705 result = CompletionCompleters.CompleteComment(completionContext, ref replacementIndex, ref replacementLength);706 break;707 708 case TokenKind.StringExpandable:709 case TokenKind.StringLiteral:710 // Search to see if we're looking at an assignment711 if (lastAst.Parent is CommandExpressionAst712 && lastAst.Parent.Parent is AssignmentStatementAst assignmentAst)713 {714 // Handle scenarios like `$ErrorActionPreference = '<tab>`715 if (TryGetCompletionsForVariableAssignment(completionContext, assignmentAst, out List<CompletionResult> completions))716 {717 return completions;718 }719 }720 else if (lastAst.Parent is BinaryExpressionAst binaryExpression)721 {722 completionContext.WordToComplete = (tokenAtCursor as StringToken).Value;723 result = CompletionCompleters.CompleteComparisonOperatorValues(completionContext, binaryExpression.Left);724 if (result.Count > 0)725 {726 return result;727 }728 }729 else if (lastAst.Parent is IndexExpressionAst indexExpressionAst)730 {731 // Handles quoted string inside index expression like: $PSVersionTable["<Tab>"]732 completionContext.WordToComplete = (tokenAtCursor as StringToken).Value;733 // Check for $PSBoundParameters indexer first734 var psBoundResult = CompleteAgainstPSBoundParametersAccess(completionContext);735 if (psBoundResult != null && psBoundResult.Count > 0)736 {737 return psBoundResult;738 }739 740 return CompletionCompleters.CompleteIndexExpression(completionContext, indexExpressionAst.Target);741 }742 743 result = GetResultForString(completionContext, ref replacementIndex, ref replacementLength);744 break;745 746 case TokenKind.RBracket:747 if (lastAst is TypeExpressionAst)748 {749 var targetExpr = (TypeExpressionAst)lastAst;750 var memberResult = new List<CompletionResult>();751 752 CompletionCompleters.CompleteMemberHelper(753 true,754 "*",755 targetExpr,756 completionContext, memberResult);757 758 if (memberResult.Count > 0)759 {760 replacementIndex++;761 replacementLength = 0;762 result = (from entry in memberResult763 let completionText = TokenKind.ColonColon.Text() + entry.CompletionText764 select new CompletionResult(completionText, entry.ListItemText, entry.ResultType, entry.ToolTip)).ToList();765 }766 }767 768 break;769 770 case TokenKind.Comma:771 // Handle array elements such as the followings:772 // - `dir .\cd,<tab>`773 // - `dir -Path: .\cd,<tab>`774 // - `dir .\abc.txt,<tab> -File`775 // - `dir -Path .\abc.txt,<tab> -File`776 // - `dir -Path: .\abc.txt,<tab> -File`777 if (lastAst is ErrorExpressionAst or ArrayLiteralAst &&778 lastAst.Parent is CommandAst or CommandParameterAst)779 {780 replacementIndex += replacementLength;781 replacementLength = 0;782 783 result = CompletionCompleters.CompleteCommandArgument(completionContext);784 }785 else if (lastAst is AttributeAst)786 {787 completionContext.ReplacementIndex = replacementIndex += tokenAtCursor.Text.Length;788 completionContext.ReplacementLength = replacementLength = 0;789 result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);790 }791 else792 {793 //794 // Handle auto completion for enum/dependson property of DSC resource,795 // cursor is right after ','796 //797 // Configuration config798 // {799 // User test800 // {801 // DependsOn=@('[user]x',|)802 //803 result = GetResultForEnumPropertyValueOfDSCResource(completionContext, string.Empty, ref replacementIndex, ref replacementLength, out _);804 }805 806 break;807 case TokenKind.AtCurly:808 // Handle scenarios such as 'Sort-Object @{<tab>' and 'gci | Format-Table @{'809 result = GetResultForHashtable(completionContext);810 replacementIndex += 2;811 replacementLength = 0;812 break;813 814 case TokenKind.Semi:815 // Handle scenarios such as 'gci | Format-Table @{Label=...;<tab>'816 if (lastAst is HashtableAst)817 {818 result = GetResultForHashtable(completionContext);819 replacementIndex += 1;820 replacementLength = 0;821 }822 823 break;824 825 case TokenKind.Number:826 // Handle scenarios such as Get-Process -Id 5<tab> || Get-Process -Id 5210, 3<tab> || Get-Process -Id: 5210, 3<tab>827 if (lastAst is ConstantExpressionAst &&828 (lastAst.Parent is CommandAst || lastAst.Parent is CommandParameterAst ||829 (lastAst.Parent is ArrayLiteralAst &&830 (lastAst.Parent.Parent is CommandAst || lastAst.Parent.Parent is CommandParameterAst))))831 {832 completionContext.WordToComplete = tokenAtCursor.Text;833 result = CompletionCompleters.CompleteCommandArgument(completionContext);834 835 replacementIndex = completionContext.ReplacementIndex;836 replacementLength = completionContext.ReplacementLength;837 }838 else if (lastAst.Parent is CommandExpressionAst839 && lastAst.Parent.Parent is AssignmentStatementAst assignmentAst2)840 {841 // Handle scenarios like '[ValidateSet(11,22)][int]$i = 11; $i = 2<tab>'842 if (TryGetCompletionsForVariableAssignment(completionContext, assignmentAst2, out List<CompletionResult> completions))843 {844 result = completions;845 }846 }847 848 break;849 850 case TokenKind.Redirection:851 // Handle file name completion after the redirection operator: gps ><tab> || gps >><tab> || dir con 2><tab> || dir con 2>><tab>852 if (lastAst is ErrorExpressionAst && lastAst.Parent is FileRedirectionAst)853 {854 completionContext.WordToComplete = string.Empty;855 completionContext.ReplacementIndex = (replacementIndex += tokenAtCursor.Text.Length);856 completionContext.ReplacementLength = replacementLength = 0;857 result = new List<CompletionResult>(CompletionCompleters.CompleteFilename(completionContext));858 }859 860 break;861 862 case TokenKind.Minus:863 // Handle operator completion: 55 -<tab> || "string" -<tab> || (Get-Something) -<tab>864 if (CompleteOperator(tokenAtCursor, lastAst))865 {866 result = CompletionCompleters.CompleteOperator(string.Empty);867 break;868 }869 870 // Handle the flag completion for statements, such as the switch statement871 if (CompleteAgainstStatementFlags(completionContext.RelatedAsts[0], null, tokenAtCursor, out statementKind))872 {873 completionContext.WordToComplete = tokenAtCursor.Text;874 result = CompletionCompleters.CompleteStatementFlags(statementKind, completionContext.WordToComplete);875 break;876 }877 878 break;879 880 case TokenKind.DynamicKeyword:881 {882 DynamicKeywordStatementAst keywordAst;883 ConfigurationDefinitionAst configureAst = GetAncestorConfigurationAstAndKeywordAst(884 completionContext.CursorPosition, lastAst, out keywordAst);885 Diagnostics.Assert(configureAst != null, "ConfigurationDefinitionAst should never be null");886 bool matched = false;887 completionContext.WordToComplete = tokenAtCursor.Text.Trim();888 // Current token is within ConfigurationDefinitionAst or DynamicKeywordStatementAst889 return GetResultForIdentifierInConfiguration(completionContext, configureAst, null, out matched);890 }891 case TokenKind.Equals:892 case TokenKind.AtParen:893 case TokenKind.LParen:894 {895 if (lastAst is AttributeAst)896 {897 completionContext.ReplacementIndex = replacementIndex += tokenAtCursor.Text.Length;898 completionContext.ReplacementLength = replacementLength = 0;899 result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);900 }901 else if (lastAst is HashtableAst hashTableAst && lastAst.Parent is not DynamicKeywordStatementAst && CheckForPendingAssignment(hashTableAst))902 {903 // Handle scenarios such as 'gci | Format-Table @{Label=<tab>' if incomplete parsing of the assignment.904 return null;905 }906 else if (lastAst is AssignmentStatementAst assignmentAst2)907 {908 completionContext.ReplacementIndex = replacementIndex += tokenAtCursor.Text.Length;909 completionContext.ReplacementLength = replacementLength = 0;910 911 // Handle scenarios like '$ErrorActionPreference =<tab>'912 if (TryGetCompletionsForVariableAssignment(completionContext, assignmentAst2, out List<CompletionResult> completions))913 {914 return completions;915 }916 }917 else if (lastAst is VariableExpressionAst && lastAst.Parent is ParameterAst paramAst && paramAst.Attributes.Count > 0)918 {919 foreach (AttributeBaseAst attribute in paramAst.Attributes)920 {921 if (IsCursorWithinOrJustAfterExtent(_cursorPosition, attribute.Extent))922 {923 completionContext.ReplacementIndex = replacementIndex += tokenAtCursor.Text.Length;924 completionContext.ReplacementLength = replacementLength = 0;925 result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);926 break;927 }928 }929 }930 else931 {932 // Handle scenarios such as 'configuration foo { File ab { Attributes ='933 // (auto completion for enum/dependson property of DSC resource),934 // cursor is right after '=', '(' or '@('935 //936 // Configuration config937 // {938 // User test939 // {940 // DependsOn=|941 // DependsOn=@(|)942 // DependsOn=(|943 //944 result = GetResultForEnumPropertyValueOfDSCResource(completionContext, string.Empty, ref replacementIndex, ref replacementLength, out _);945 }946 947 break;948 }949 950 case TokenKind.Format:951 case TokenKind.Not:952 case TokenKind.Bnot:953 case TokenKind.And:954 case TokenKind.Or:955 case TokenKind.Xor:956 case TokenKind.Band:957 case TokenKind.Bor:958 case TokenKind.Bxor:959 case TokenKind.Join:960 case TokenKind.Ieq:961 case TokenKind.Ine:962 case TokenKind.Ige:963 case TokenKind.Igt:964 case TokenKind.Ilt:965 case TokenKind.Ile:966 case TokenKind.Ilike:967 case TokenKind.Inotlike:968 case TokenKind.Imatch:969 case TokenKind.Inotmatch:970 case TokenKind.Ireplace:971 case TokenKind.Icontains:972 case TokenKind.Inotcontains:973 case TokenKind.Iin:974 case TokenKind.Inotin:975 case TokenKind.Isplit:976 case TokenKind.Ceq:977 case TokenKind.Cne:978 case TokenKind.Cge:979 case TokenKind.Cgt:980 case TokenKind.Clt:981 case TokenKind.Cle:982 case TokenKind.Clike:983 case TokenKind.Cnotlike:984 case TokenKind.Cmatch:985 case TokenKind.Cnotmatch:986 case TokenKind.Creplace:987 case TokenKind.Ccontains:988 case TokenKind.Cnotcontains:989 case TokenKind.Cin:990 case TokenKind.Cnotin:991 case TokenKind.Csplit:992 case TokenKind.Is:993 case TokenKind.IsNot:994 case TokenKind.As:995 case TokenKind.Shl:996 case TokenKind.Shr:997 result = CompletionCompleters.CompleteOperator(tokenAtCursor.Text);998 break;999 1000 case TokenKind.LBracket:1001 if (lastAst.Parent is IndexExpressionAst indexExpression)1002 {1003 // Handles index expression with cursor right after lbracket like: $PSVersionTable[<Tab>]1004 completionContext.WordToComplete = string.Empty;1005 result = CompletionCompleters.CompleteIndexExpression(completionContext, indexExpression.Target);1006 if (result.Count > 0)1007 {1008 replacementIndex++;1009 replacementLength--;1010 }1011 }1012 break;1013 default:1014 result = CompleteUsingKeywords(completionContext.CursorPosition.Offset, _tokens, ref replacementIndex, ref replacementLength);1015 if (result is not null)1016 {1017 return result;1018 }1019 1020 if ((tokenAtCursor.TokenFlags & TokenFlags.Keyword) != 0)1021 {1022 completionContext.WordToComplete = tokenAtCursor.Text;1023 1024 // Handle the file name completion1025 result = CompleteFileNameAsCommand(completionContext);1026 1027 // Handle the command name completion1028 var commandNameResult = CompletionCompleters.CompleteCommand(completionContext);1029 if (commandNameResult != null && commandNameResult.Count > 0)1030 {1031 result.AddRange(commandNameResult);1032 }1033 }1034 else1035 {1036 replacementIndex = -1;1037 replacementLength = -1;1038 }1039 1040 break;1041 }1042 }1043 else1044 {1045 IScriptPosition cursor = completionContext.CursorPosition;1046 bool isCursorLineEmpty = string.IsNullOrWhiteSpace(cursor.Line);1047 var tokenBeforeCursor = completionContext.TokenBeforeCursor;1048 bool isLineContinuationBeforeCursor = false;1049 if (tokenBeforeCursor != null)1050 {1051 //1052 // Handle following scenario, cursor is in next line and after a command call,1053 // we need to skip the command call autocompletion if there is no backtick character1054 // in the end of the previous line, since backtick means command call continues to the next line1055 //1056 // Configuration config1057 // {1058 // User test1059 // {1060 // DependsOn=zzz1061 // |1062 //1063 isLineContinuationBeforeCursor = completionContext.TokenBeforeCursor.Kind == TokenKind.LineContinuation;1064 }1065 1066 bool skipAutoCompleteForCommandCall = isCursorLineEmpty && !isLineContinuationBeforeCursor;1067 bool lastAstIsExpressionAst = lastAst is ExpressionAst;1068 if (!skipAutoCompleteForCommandCall &&1069 (lastAst is CommandParameterAst || lastAst is CommandAst ||1070 (lastAstIsExpressionAst && lastAst.Parent is CommandAst) ||1071 (lastAstIsExpressionAst && lastAst.Parent is CommandParameterAst) ||1072 (lastAstIsExpressionAst && lastAst.Parent is ArrayLiteralAst &&1073 (lastAst.Parent.Parent is CommandAst || lastAst.Parent.Parent is CommandParameterAst))))1074 {1075 completionContext.WordToComplete = string.Empty;1076 1077 var hashTableAst = lastAst as HashtableAst;1078 1079 // Do not do any tab completion if we have a hash table1080 // and an assignment is pending. For cases like:1081 // new-object System.Drawing.Point -prop @{ X= -> Tab should not complete1082 // Note: This check works when all statements preceding the last are complete,1083 // but if a preceding statement is incomplete this test fails because1084 // the Ast mixes the statements due to incomplete parsing.1085 // e.g.,1086 // new-object System.Drawing.Point -prop @{ X = 100; Y = <- Incomplete line1087 // new-object new-object System.Drawing.Point -prop @{ X = <- Tab will yield hash properties.1088 if (hashTableAst != null &&1089 CheckForPendingAssignment(hashTableAst))1090 {1091 return result;1092 }1093 1094 if (hashTableAst != null)1095 {1096 completionContext.ReplacementIndex = replacementIndex = completionContext.CursorPosition.Offset;1097 completionContext.ReplacementLength = replacementLength = 0;1098 result = CompletionCompleters.CompleteHashtableKey(completionContext, hashTableAst);1099 }1100 else1101 {1102 result = CompletionCompleters.CompleteCommandArgument(completionContext);1103 replacementIndex = completionContext.ReplacementIndex;1104 replacementLength = completionContext.ReplacementLength;1105 }1106 }1107 else1108 {1109 //1110 // Handle completion of empty line within configuration statement1111 // Ignore the auto completion if there is a backtick character in previous line1112 //1113 bool cursorAtLineContinuation;1114 if ((tokenAtCursor != null && tokenAtCursor.Kind == TokenKind.LineContinuation) ||1115 (tokenBeforeCursor != null && tokenBeforeCursor.Kind == TokenKind.LineContinuation))1116 cursorAtLineContinuation = true;1117 else1118 cursorAtLineContinuation = false;1119 if (isCursorLineEmpty && !cursorAtLineContinuation)1120 {1121 //1122 // Handle following scenario, both Configuration and DSC resource 'User' are not complete1123 // Check Hashtable first, and then fallback to configuration1124 //1125 // Configuration config1126 // {1127 // User test1128 // {1129 // DependsOn=''1130 // |1131 result = GetResultForHashtable(completionContext);1132 if (result == null || result.Count == 0)1133 {1134 DynamicKeywordStatementAst keywordAst;1135 ConfigurationDefinitionAst configAst = GetAncestorConfigurationAstAndKeywordAst(cursor, lastAst, out keywordAst);1136 if (configAst != null)1137 {1138 bool matched;1139 result = GetResultForIdentifierInConfiguration(completionContext, configAst, keywordAst, out matched);1140 }1141 }1142 1143 // Handles following scenario where user is tab completing a member on an empty line:1144 // "Hello".1145 // <Tab>1146 if ((result is null || result.Count == 0) && tokenBeforeCursor is not null)1147 {1148 switch (completionContext.TokenBeforeCursor.Kind)1149 {1150 1151 case TokenKind.Dot:1152 case TokenKind.ColonColon:1153 case TokenKind.QuestionDot:1154 replacementIndex = cursor.Offset;1155 replacementLength = 0;1156 result = CompletionCompleters.CompleteMember(completionContext, @static: completionContext.TokenBeforeCursor.Kind == TokenKind.ColonColon, ref replacementLength);1157 break;1158 1159 case TokenKind.LParen:1160 case TokenKind.Comma:1161 if (lastAst is AttributeAst)1162 {1163 result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);1164 }1165 1166 if (lastAst is VariableExpressionAst && lastAst.Parent is ParameterAst paramAst && paramAst.Attributes.Count > 0)1167 {1168 foreach (AttributeBaseAst attribute in paramAst.Attributes)1169 {1170 if (IsCursorWithinOrJustAfterExtent(_cursorPosition, attribute.Extent))1171 {1172 result = GetResultForAttributeArgument(completionContext, ref replacementIndex, ref replacementLength);1173 break;1174 }1175 }1176 }1177 break;1178 1179 case TokenKind.Ieq:1180 case TokenKind.Ceq:1181 case TokenKind.Ine:1182 case TokenKind.Cne:1183 case TokenKind.Ilike:1184 case TokenKind.Clike:1185 case TokenKind.Inotlike:1186 case TokenKind.Cnotlike:1187 case TokenKind.Imatch:1188 case TokenKind.Cmatch:1189 case TokenKind.Inotmatch:1190 case TokenKind.Cnotmatch:1191 if (lastAst is BinaryExpressionAst binaryExpression)1192 {1193 completionContext.WordToComplete = string.Empty;1194 result = CompletionCompleters.CompleteComparisonOperatorValues(completionContext, binaryExpression.Left);1195 }1196 break;1197 1198 case TokenKind.LBracket:1199 if (lastAst.Parent is IndexExpressionAst indexExpression)1200 {