MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Collections;5using System.Collections.Generic;6using System.Collections.ObjectModel;7using System.Diagnostics.CodeAnalysis;8using System.Management.Automation.Language;9using System.Management.Automation.Runspaces;10#if LEGACYTELEMETRY11using System.Diagnostics;12using Microsoft.PowerShell.Telemetry.Internal;13#endif14 15namespace System.Management.Automation16{17 /// <summary>18 /// Provides a set of possible completions for given input.19 /// </summary>20 public class CommandCompletion21 {22 /// <summary>23 /// Construct the result CompleteInput or TabExpansion2.24 /// </summary>25 public CommandCompletion(Collection<CompletionResult> matches, int currentMatchIndex, int replacementIndex, int replacementLength)26 {27 this.CompletionMatches = matches;28 this.CurrentMatchIndex = currentMatchIndex;29 this.ReplacementIndex = replacementIndex;30 this.ReplacementLength = replacementLength;31 }32 33 #region Fields and Properties34 35 /// <summary>36 /// Current index in <see cref="CompletionMatches"/>.37 /// </summary>38 public int CurrentMatchIndex { get; set; }39 40 /// <summary>41 /// Returns the starting replacement index from the original input.42 /// </summary>43 public int ReplacementIndex { get; set; }44 45 /// <summary>46 /// Returns the length of the text to replace from the original input.47 /// </summary>48 public int ReplacementLength { get; set; }49 50 /// <summary>51 /// Gets all the completion results.52 /// </summary>53 [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]54 public Collection<CompletionResult> CompletionMatches { get; set; }55 56 internal static readonly IList<CompletionResult> EmptyCompletionResult = Array.Empty<CompletionResult>();57 58 private static readonly CommandCompletion s_emptyCommandCompletion = new CommandCompletion(59 new Collection<CompletionResult>(EmptyCompletionResult), -1, -1, -1);60 61 #endregion Fields and Properties62 63 #region public methods64 65 /// <summary>66 /// </summary>67 /// <param name="input"></param>68 /// <param name="cursorIndex"></param>69 /// <returns></returns>70 public static Tuple<Ast, Token[], IScriptPosition> MapStringInputToParsedInput(string input, int cursorIndex)71 {72 if (cursorIndex > input.Length)73 {74 throw PSTraceSource.NewArgumentException(nameof(cursorIndex));75 }76 77 Token[] tokens;78 ParseError[] errors;79 var ast = Parser.ParseInput(input, out tokens, out errors);80 81 IScriptPosition cursorPosition =82 ((InternalScriptPosition)ast.Extent.StartScriptPosition).CloneWithNewOffset(cursorIndex);83 return Tuple.Create<Ast, Token[], IScriptPosition>(ast, tokens, cursorPosition);84 }85 86 /// <summary>87 /// </summary>88 /// <param name="input">The input to complete.</param>89 /// <param name="cursorIndex">The index of the cursor in the input.</param>90 /// <param name="options">Optional options to configure how completion is performed.</param>91 /// <returns></returns>92 public static CommandCompletion CompleteInput(string input, int cursorIndex, Hashtable options)93 {94 if (input == null || input.Length == 0)95 {96 return s_emptyCommandCompletion;97 }98 99 var parsedInput = MapStringInputToParsedInput(input, cursorIndex);100 return CompleteInputImpl(parsedInput.Item1, parsedInput.Item2, parsedInput.Item3, options);101 }102 103 /// <summary>104 /// </summary>105 /// <param name="ast">Ast for pre-parsed input.</param>106 /// <param name="tokens">Tokens for pre-parsed input.</param>107 /// <param name="positionOfCursor"></param>108 /// <param name="options">Optional options to configure how completion is performed.</param>109 /// <returns></returns>110 public static CommandCompletion CompleteInput(Ast ast, Token[] tokens, IScriptPosition positionOfCursor, Hashtable options)111 {112 if (ast == null)113 {114 throw PSTraceSource.NewArgumentNullException(nameof(ast));115 }116 117 if (tokens == null)118 {119 throw PSTraceSource.NewArgumentNullException(nameof(tokens));120 }121 122 if (positionOfCursor == null)123 {124 throw PSTraceSource.NewArgumentNullException(nameof(positionOfCursor));125 }126 127 if (ast.Extent.Text.Length == 0)128 {129 return s_emptyCommandCompletion;130 }131 132 return CompleteInputImpl(ast, tokens, positionOfCursor, options);133 }134 135 /// <summary>136 /// Invokes the script function TabExpansion2.137 /// </summary>138 /// <param name="input">The input script to complete.</param>139 /// <param name="cursorIndex">The offset in <paramref name="input"/> where completion is requested.</param>140 /// <param name="options">Optional parameter that specifies configurable options for completion.</param>141 /// <param name="powershell">The powershell to use to invoke the script function TabExpansion2.</param>142 /// <returns>A collection of completions with the replacement start and length.</returns>143 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "powershell")]144 public static CommandCompletion CompleteInput(string input, int cursorIndex, Hashtable options, PowerShell powershell)145 {146 if (input == null || input.Length == 0)147 {148 return s_emptyCommandCompletion;149 }150 151 if (cursorIndex > input.Length)152 {153 throw PSTraceSource.NewArgumentException(nameof(cursorIndex));154 }155 156 if (powershell == null)157 {158 throw PSTraceSource.NewArgumentNullException(nameof(powershell));159 }160 161 // If we are in a debugger stop, let the debugger do the command completion.162 var debugger = powershell.Runspace?.Debugger;163 if ((debugger != null) && debugger.InBreakpoint)164 {165 return CompleteInputInDebugger(input, cursorIndex, options, debugger);166 }167 168 var remoteRunspace = powershell.Runspace as RemoteRunspace;169 if (remoteRunspace != null)170 {171 // If the runspace is not available to run commands then exit here because nested commands are not172 // supported on remote runspaces.173 if (powershell.IsNested || (remoteRunspace.RunspaceAvailability != RunspaceAvailability.Available))174 {175 return s_emptyCommandCompletion;176 }177 178 // If it's in the nested prompt, the powershell instance is created by "PowerShell.Create(RunspaceMode.CurrentRunspace);".179 // In this case, the powershell._runspace is null but if we try to access the property "Runspace", it will create a new180 // local runspace - the default local runspace will be abandoned. So we check the powershell.IsChild first to make sure181 // not to access the property "Runspace" in this case - powershell.isChild will be set to true only in this case.182 if (!powershell.IsChild)183 {184 CheckScriptCallOnRemoteRunspace(remoteRunspace);185 186 // TabExpansion2 script is not available prior to PSv3.187 if (remoteRunspace.GetCapabilities().Equals(Runspaces.RunspaceCapability.Default))188 {189 return s_emptyCommandCompletion;190 }191 }192 }193 194 return CallScriptWithStringParameterSet(input, cursorIndex, options, powershell);195 }196 197 /// <summary>198 /// Invokes the script function TabExpansion2.199 /// </summary>200 /// <param name="ast">The ast for pre-parsed input.</param>201 /// <param name="tokens"></param>202 /// <param name="cursorPosition"></param>203 /// <param name="options">Optional options to configure how completion is performed.</param>204 /// <param name="powershell">The powershell to use to invoke the script function TabExpansion2.</param>205 /// <returns></returns>206 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "powershell")]207 public static CommandCompletion CompleteInput(Ast ast, Token[] tokens, IScriptPosition cursorPosition, Hashtable options, PowerShell powershell)208 {209 if (ast == null)210 {211 throw PSTraceSource.NewArgumentNullException(nameof(ast));212 }213 214 if (tokens == null)215 {216 throw PSTraceSource.NewArgumentNullException(nameof(tokens));217 }218 219 if (cursorPosition == null)220 {221 throw PSTraceSource.NewArgumentNullException(nameof(cursorPosition));222 }223 224 if (powershell == null)225 {226 throw PSTraceSource.NewArgumentNullException(nameof(powershell));227 }228 229 if (ast.Extent.Text.Length == 0)230 {231 return s_emptyCommandCompletion;232 }233 234 // If we are in a debugger stop, let the debugger do the command completion.235 var debugger = powershell.Runspace?.Debugger;236 if ((debugger != null) && debugger.InBreakpoint)237 {238 return CompleteInputInDebugger(ast, tokens, cursorPosition, options, debugger);239 }240 241 var remoteRunspace = powershell.Runspace as RemoteRunspace;242 if (remoteRunspace != null)243 {244 // If the runspace is not available to run commands then exit here because nested commands are not245 // supported on remote runspaces.246 if (powershell.IsNested || (remoteRunspace.RunspaceAvailability != RunspaceAvailability.Available))247 {248 return s_emptyCommandCompletion;249 }250 251 if (!powershell.IsChild)252 {253 CheckScriptCallOnRemoteRunspace(remoteRunspace);254 255 // TabExpansion2 script is not available prior to PSv3.256 if (remoteRunspace.GetCapabilities().Equals(Runspaces.RunspaceCapability.Default))257 {258 return s_emptyCommandCompletion;259 }260 261 // When calling the TabExpansion2 script, the input should be the whole script text262 string input = ast.Extent.Text;263 int cursorIndex = ((InternalScriptPosition)cursorPosition).Offset;264 return CallScriptWithStringParameterSet(input, cursorIndex, options, powershell);265 }266 }267 268 return CallScriptWithAstParameterSet(ast, tokens, cursorPosition, options, powershell);269 }270 271 /// <summary>272 /// Get the next result, moving forward or backward. Supports wraparound, so if there are any results at all,273 /// this method will never fail and never return null.274 /// </summary>275 /// <param name="forward">True if we should move forward through the list, false if backwards.</param>276 /// <returns>The next completion result, or null if no results.</returns>277 public CompletionResult GetNextResult(bool forward)278 {279 CompletionResult result = null;280 var count = CompletionMatches.Count;281 if (count > 0)282 {283 CurrentMatchIndex += forward ? 1 : -1;284 if (CurrentMatchIndex >= count)285 {286 CurrentMatchIndex = 0;287 }288 else if (CurrentMatchIndex < 0)289 {290 CurrentMatchIndex = count - 1;291 }292 293 result = CompletionMatches[CurrentMatchIndex];294 }295 296 return result;297 }298 299 #endregion public methods300 301 #region Internal methods302 303 /// <summary>304 /// Command completion while in debug break mode.305 /// </summary>306 /// <param name="input">The input script to complete.</param>307 /// <param name="cursorIndex">The offset in <paramref name="input"/> where completion is requested.</param>308 /// <param name="options">Optional parameter that specifies configurable options for completion.</param>309 /// <param name="debugger">Current debugger.</param>310 /// <returns>A collection of completions with the replacement start and length.</returns>311 internal static CommandCompletion CompleteInputInDebugger(string input, int cursorIndex, Hashtable options, Debugger debugger)312 {313 if (input == null)314 {315 return s_emptyCommandCompletion;316 }317 318 if (cursorIndex > input.Length)319 {320 throw PSTraceSource.NewArgumentException(nameof(cursorIndex));321 }322 323 if (debugger == null)324 {325 throw PSTraceSource.NewArgumentNullException(nameof(debugger));326 }327 328 Command cmd = new Command("TabExpansion2");329 cmd.Parameters.Add("InputScript", input);330 cmd.Parameters.Add("CursorColumn", cursorIndex);331 cmd.Parameters.Add("Options", options);332 333 return ProcessCompleteInputCommand(cmd, debugger);334 }335 336 /// <summary>337 /// Command completion while in debug break mode.338 /// </summary>339 /// <param name="ast">The ast for pre-parsed input.</param>340 /// <param name="tokens"></param>341 /// <param name="cursorPosition"></param>342 /// <param name="options">Optional options to configure how completion is performed.</param>343 /// <param name="debugger">Current debugger.</param>344 /// <returns>Command completion.</returns>345 internal static CommandCompletion CompleteInputInDebugger(Ast ast, Token[] tokens, IScriptPosition cursorPosition, Hashtable options, Debugger debugger)346 {347 if (ast == null)348 {349 throw PSTraceSource.NewArgumentNullException(nameof(ast));350 }351 352 if (tokens == null)353 {354 throw PSTraceSource.NewArgumentNullException(nameof(tokens));355 }356 357 if (cursorPosition == null)358 {359 throw PSTraceSource.NewArgumentNullException(nameof(cursorPosition));360 }361 362 if (debugger == null)363 {364 throw PSTraceSource.NewArgumentNullException(nameof(debugger));365 }366 367 // For remote debugging just pass string input.368 if ((debugger is RemoteDebugger) || debugger.IsPushed)369 {370 string input = ast.Extent.Text;371 int cursorIndex = ((InternalScriptPosition)cursorPosition).Offset;372 373 return CompleteInputInDebugger(input, cursorIndex, options, debugger);374 }375 376 Command cmd = new Command("TabExpansion2");377 cmd.Parameters.Add("Ast", ast);378 cmd.Parameters.Add("Tokens", tokens);379 cmd.Parameters.Add("PositionOfCursor", cursorPosition);380 cmd.Parameters.Add("Options", options);381 382 return ProcessCompleteInputCommand(cmd, debugger);383 }384 385 private static CommandCompletion ProcessCompleteInputCommand(386 Command cmd,387 Debugger debugger)388 {389 PSCommand command = new PSCommand(cmd);390 PSDataCollection<PSObject> output = new PSDataCollection<PSObject>();391 392 debugger.ProcessCommand(command, output);393 394 if (output.Count == 1)395 {396 var commandCompletion = output[0].BaseObject as CommandCompletion;397 if (commandCompletion != null)398 {399 return commandCompletion;400 }401 }402 403 return s_emptyCommandCompletion;404 }405 406 #endregion407 408 #region private methods409 410 private static void CheckScriptCallOnRemoteRunspace(RemoteRunspace remoteRunspace)411 {412 var remoteRunspaceInternal = remoteRunspace.RunspacePool.RemoteRunspacePoolInternal;413 if (remoteRunspaceInternal != null)414 {415 var transportManager = remoteRunspaceInternal.DataStructureHandler.TransportManager;416 if (transportManager != null && transportManager.TypeTable == null)417 {418 // The remote runspace was created without a TypeTable instance.419 // The tab completion results cannot be deserialized if the TypeTable is not available420 throw PSTraceSource.NewInvalidOperationException(TabCompletionStrings.CannotDeserializeTabCompletionResult);421 }422 }423 }424 425 private static CommandCompletion CallScriptWithStringParameterSet(string input, int cursorIndex, Hashtable options, PowerShell powershell)426 {427 try428 {429 powershell.Commands.Clear();430 powershell.AddCommand("TabExpansion2")431 .AddArgument(input)432 .AddArgument(cursorIndex)433 .AddArgument(options);434 var results = powershell.Invoke();435 if (results == null)436 {437 return s_emptyCommandCompletion;438 }439 440 if (results.Count == 1)441 {442 var result = PSObject.Base(results[0]);443 var commandCompletion = result as CommandCompletion;444 if (commandCompletion != null)445 {446 return commandCompletion;447 }448 }449 }450 catch (Exception)451 {452 }453 finally454 {455 powershell.Commands.Clear();456 }457 458 return s_emptyCommandCompletion;459 }460 461 private static CommandCompletion CallScriptWithAstParameterSet(Ast ast, Token[] tokens, IScriptPosition cursorPosition, Hashtable options, PowerShell powershell)462 {463 try464 {465 powershell.Commands.Clear();466 powershell.AddCommand("TabExpansion2")467 .AddArgument(ast)468 .AddArgument(tokens)469 .AddArgument(cursorPosition)470 .AddArgument(options);471 var results = powershell.Invoke();472 if (results == null)473 {474 return s_emptyCommandCompletion;475 }476 477 if (results.Count == 1)478 {479 var result = PSObject.Base(results[0]);480 var commandCompletion = result as CommandCompletion;481 if (commandCompletion != null)482 {483 return commandCompletion;484 }485 }486 }487 catch (Exception)488 {489 }490 finally491 {492 powershell.Commands.Clear();493 }494 495 return s_emptyCommandCompletion;496 }497 498 // This is the start of the real implementation of autocomplete/intellisense/tab completion499 private static CommandCompletion CompleteInputImpl(Ast ast, Token[] tokens, IScriptPosition positionOfCursor, Hashtable options)500 {501#if LEGACYTELEMETRY502 // We could start collecting telemetry at a later date.503 // We will leave the #if to remind us that we did this once.504 var sw = new Stopwatch();505 sw.Start();506#endif507 using (var powershell = PowerShell.Create(RunspaceMode.CurrentRunspace))508 {509 var context = LocalPipeline.GetExecutionContextFromTLS();510 511 int replacementIndex = -1;512 int replacementLength = -1;513 List<CompletionResult> results = null;514 515 {516 // If we were invoked from TabExpansion2, we want to "remove" TabExpansion2 and anything it calls517 // from our results. We do this by faking out the session so that TabExpansion2 isn't anywhere to be found.518 SessionStateScope scopeToRestore;519 if (context.CurrentCommandProcessor is not null520 && context.CurrentCommandProcessor.Command.CommandInfo.Name.Equals("TabExpansion2", StringComparison.OrdinalIgnoreCase)521 && context.CurrentCommandProcessor.UseLocalScope522 && context.EngineSessionState.CurrentScope.Parent is not null)523 {524 scopeToRestore = context.EngineSessionState.CurrentScope;525 context.EngineSessionState.CurrentScope = scopeToRestore.Parent;526 }527 else528 {529 scopeToRestore = null;530 }531 532 try533 {534 var completionAnalysis = new CompletionAnalysis(ast, tokens, positionOfCursor, options);535 results = completionAnalysis.GetResults(powershell, out replacementIndex, out replacementLength);536 }537 finally538 {539 if (scopeToRestore != null)540 {541 context.EngineSessionState.CurrentScope = scopeToRestore;542 }543 }544 }545 546 var completionResults = results ?? EmptyCompletionResult;547 548#if LEGACYTELEMETRY549 // no telemetry here. We don't capture tab completion performance.550 sw.Stop();551 TelemetryAPI.ReportTabCompletionTelemetry(sw.ElapsedMilliseconds, completionResults.Count,552 completionResults.Count > 0 ? completionResults[0].ResultType : CompletionResultType.Text);553#endif554 return new CommandCompletion(555 new Collection<CompletionResult>(completionResults),556 -1,557 replacementIndex,558 replacementLength);559 }560 }561 562 #endregion private methods563 }564}565 