/// <inheritdoc/> /// <remarks> /// Tries to get the suggestions for the user input from the command history. If that doesn't find /// <paramref name="suggestionCount"/> suggestions, it'll fallback to find the suggestion regardless of command history. /// </remarks> public virtual CommandLineSuggestion GetSuggestion(PredictionContext context, int suggestionCount, int maxAllowedCommandDuplicate, CancellationToken cancellationToken) { Validation.CheckArgument(context, $"{nameof(context)} cannot be null"); Validation.CheckArgument <ArgumentOutOfRangeException>(suggestionCount > 0, $"{nameof(suggestionCount)} must be larger than 0."); Validation.CheckArgument <ArgumentOutOfRangeException>(maxAllowedCommandDuplicate > 0, $"{nameof(maxAllowedCommandDuplicate)} must be larger than 0."); var relatedAsts = context.RelatedAsts; CommandAst commandAst = null; for (var i = relatedAsts.Count - 1; i >= 0; --i) { if (relatedAsts[i] is CommandAst c) { commandAst = c; break; } } if (commandAst == null) { return(null); } var commandName = commandAst.GetCommandName(); if (string.IsNullOrWhiteSpace(commandName)) { return(null); } ParameterSet inputParameterSet = null; try { inputParameterSet = new ParameterSet(commandAst, _azContext); } catch when(!IsSupportedCommand(commandName)) { // We only ignore the exception when the command name is not supported. // We want to collect the telemetry about the exception how common it is for the format we don't support. return(null); } cancellationToken.ThrowIfCancellationRequested(); var rawUserInput = context.InputAst.ToString(); var presentCommands = new Dictionary <string, int>(); var commandBasedPredictor = _commandBasedPredictor; var commandToRequestPrediction = _commandToRequestPrediction; var result = commandBasedPredictor?.Item2?.GetSuggestion(commandName, inputParameterSet, rawUserInput, presentCommands, suggestionCount, maxAllowedCommandDuplicate, cancellationToken); if ((result != null) && (result.Count > 0)) { var suggestionSource = SuggestionSource.PreviousCommand; if (string.Equals(commandToRequestPrediction, commandBasedPredictor?.Item1, StringComparison.Ordinal)) { suggestionSource = SuggestionSource.CurrentCommand; } for (var i = 0; i < result.Count; ++i) { result.UpdateSuggestionSource(i, suggestionSource); } } if ((result == null) || (result.Count < suggestionCount)) { var fallbackPredictor = _fallbackPredictor; var suggestionCountToRequest = (result == null) ? suggestionCount : suggestionCount - result.Count; var resultsFromFallback = fallbackPredictor?.GetSuggestion(commandName, inputParameterSet, rawUserInput, presentCommands, suggestionCountToRequest, maxAllowedCommandDuplicate, cancellationToken); if ((result == null) && (resultsFromFallback != null)) { result = resultsFromFallback; for (var i = 0; i < result.Count; ++i) { result.UpdateSuggestionSource(i, SuggestionSource.StaticCommands); } } else if ((resultsFromFallback != null) && (resultsFromFallback.Count > 0)) { for (var i = 0; i < resultsFromFallback.Count; ++i) { result.AddSuggestion(resultsFromFallback.PredictiveSuggestions[i], resultsFromFallback.SourceTexts[i], SuggestionSource.StaticCommands); } } } return(result); }
/// <summary> /// Returns suggestions given the user input. /// </summary> /// <param name="inputCommandName">The command name extracted from the user input.</param> /// <param name="inputParameterSet">The parameter set extracted from the user input.</param> /// <param name="rawUserInput">The string format of the command line from user input.</param> /// <param name="presentCommands">Commands already present. Contents may be added to this collection.</param> /// <param name="suggestionCount">The number of suggestions to return.</param> /// <param name="maxAllowedCommandDuplicate">The maximum amount of the same commands in the list of predictions.</param> /// <param name="cancellationToken">The cancellation token</param> /// <returns>The collections of suggestions.</returns> public CommandLineSuggestion GetSuggestion(string inputCommandName, ParameterSet inputParameterSet, string rawUserInput, IDictionary <string, int> presentCommands, int suggestionCount, int maxAllowedCommandDuplicate, CancellationToken cancellationToken) { Validation.CheckArgument(!string.IsNullOrWhiteSpace(inputCommandName), $"{nameof(inputCommandName)} cannot be null or whitespace."); Validation.CheckArgument(inputParameterSet, $"{nameof(inputParameterSet)} cannot be null."); Validation.CheckArgument(!string.IsNullOrWhiteSpace(rawUserInput), $"{nameof(rawUserInput)} cannot be null or whitespace."); Validation.CheckArgument(presentCommands, $"{nameof(presentCommands)} cannot be null."); Validation.CheckArgument <ArgumentOutOfRangeException>(suggestionCount > 0, $"{nameof(suggestionCount)} must be larger than 0."); Validation.CheckArgument <ArgumentOutOfRangeException>(maxAllowedCommandDuplicate > 0, $"{nameof(maxAllowedCommandDuplicate)} must be larger than 0."); const int commandCollectionCapacity = 10; CommandLineSuggestion result = new(); var duplicateResults = new Dictionary <string, DuplicateResult>(commandCollectionCapacity, StringComparer.OrdinalIgnoreCase); var isCommandNameComplete = inputParameterSet.Parameters.Any() || rawUserInput.EndsWith(' '); Func <string, bool> commandNameQuery = (command) => command.Equals(inputCommandName, StringComparison.OrdinalIgnoreCase); if (!isCommandNameComplete) { commandNameQuery = (command) => command.StartsWith(inputCommandName, StringComparison.OrdinalIgnoreCase); } // Try to find the matching command and arrange the parameters in the order of the input. // // Predictions should be flexible, e.g. if "Command -Name N -Location L" is a possibility, // then "Command -Location L -Name N" should also be possible. // // resultBuilder and usedParams are used to store the information to construct the result. // We want to avoid too much heap allocation for the performance purpose. const int parameterCollectionCapacity = 10; var resultBuilder = new StringBuilder(); var usedParams = new HashSet <int>(parameterCollectionCapacity); var sourceBuilder = new StringBuilder(); for (var i = 0; i < _commandLinePredictions.Count && result.Count < suggestionCount; ++i) { if (commandNameQuery(_commandLinePredictions[i].Name)) { cancellationToken.ThrowIfCancellationRequested(); resultBuilder.Clear(); resultBuilder.Append(_commandLinePredictions[i].Name); usedParams.Clear(); string commandNoun = ParameterValuePredictor.GetAzCommandNoun(_commandLinePredictions[i].Name).ToLower(); if (DoesPredictionParameterSetMatchInput(resultBuilder, inputParameterSet, commandNoun, _commandLinePredictions[i].ParameterSet, usedParams)) { PredictRestOfParameters(resultBuilder, commandNoun, _commandLinePredictions[i].ParameterSet.Parameters, usedParams); if (resultBuilder.Length <= rawUserInput.Length) { continue; } var prediction = resultBuilder.ToString(); sourceBuilder.Clear(); sourceBuilder.Append(_commandLinePredictions[i].Name); foreach (var p in _commandLinePredictions[i].ParameterSet.Parameters) { AppendParameterNameAndValue(sourceBuilder, p.Name, p.Value); } if (!presentCommands.ContainsKey(_commandLinePredictions[i].Name)) { result.AddSuggestion(new PredictiveSuggestion(prediction, _commandLinePredictions[i].Description), sourceBuilder.ToString()); presentCommands.Add(_commandLinePredictions[i].Name, 1); } else if (presentCommands[_commandLinePredictions[i].Name] < maxAllowedCommandDuplicate) { result.AddSuggestion(new PredictiveSuggestion(prediction, _commandLinePredictions[i].Description), sourceBuilder.ToString()); presentCommands[_commandLinePredictions[i].Name] += 1; } else { _ = duplicateResults.TryAdd(prediction, new DuplicateResult(sourceBuilder.ToString(), _commandLinePredictions[i].Description)); } } } } var resultCount = result.Count; if ((resultCount < suggestionCount) && (duplicateResults.Count > 0)) { foreach (var temp in duplicateResults.Take(suggestionCount - resultCount)) { result.AddSuggestion(new PredictiveSuggestion(temp.Key, temp.Value.Description), temp.Value.Source); } } return(result); }
/// <summary> /// Determines if parameter set contains all of the parameters of the input. /// </summary> /// <param name="builder">StringBuilder that aggregates the prediction text output.</param> /// <param name="inputParameters">Parsed ParameterSet from the user input AST.</param> /// <param name="commandNoun">Command Noun.</param> /// <param name="predictionParameters">Candidate prediction parameter set.</param> /// <param name="usedParams">Set of used parameters for set.</param> private bool DoesPredictionParameterSetMatchInput(StringBuilder builder, ParameterSet inputParameters, string commandNoun, ParameterSet predictionParameters, HashSet <int> usedParams) { foreach (var inputParameter in inputParameters.Parameters) { var matchIndex = FindParameterPositionInSet(inputParameter, predictionParameters, usedParams); if (matchIndex == -1) { return(false); } else { usedParams.Add(matchIndex); if (inputParameter.Value != null) { AppendParameterNameAndValue(builder, predictionParameters.Parameters[matchIndex].Name, inputParameter.Value); } else { BuildParameterValue(builder, commandNoun, predictionParameters.Parameters[matchIndex]); } } } return(true); }
/// <inheritdoc/> /// <remarks> /// Tries to get the suggestions for the user input from the command history. If that doesn't find /// <paramref name="suggestionCount"/> suggestions, it'll fallback to find the suggestion regardless of command history. /// </remarks> public virtual CommandLineSuggestion GetSuggestion(PredictionContext context, int suggestionCount, int maxAllowedCommandDuplicate, CancellationToken cancellationToken) { Validation.CheckArgument(context, $"{nameof(context)} cannot be null"); Validation.CheckArgument <ArgumentOutOfRangeException>(suggestionCount > 0, $"{nameof(suggestionCount)} must be larger than 0."); Validation.CheckArgument <ArgumentOutOfRangeException>(maxAllowedCommandDuplicate > 0, $"{nameof(maxAllowedCommandDuplicate)} must be larger than 0."); var relatedAsts = context.RelatedAsts; CommandAst commandAst = null; for (var i = relatedAsts.Count - 1; i >= 0; --i) { if (relatedAsts[i] is CommandAst c) { commandAst = c; break; } } if (commandAst == null) { return(null); } var commandName = commandAst.GetCommandName(); if (string.IsNullOrWhiteSpace(commandName)) { return(null); } ParameterSet inputParameterSet = null; try { inputParameterSet = new ParameterSet(commandAst, _azContext); } catch when(!IsSupportedCommand(commandName)) { // We only ignore the exception when the command name is not supported. // We want to collect the telemetry about the exception how common it is for the format we don't support. return(null); } cancellationToken.ThrowIfCancellationRequested(); // We want to show a survey/feedback cmdlet at the end of the suggestion list. We try to find one less // suggestions to make room for that cmdlet and avoid too much computation. // But if only one suggestion is requested, we don't replace it with the survey cmdlets. var suggestionFromPredictorCount = (suggestionCount == 1) ? 1 : (suggestionCount - 1); var rawUserInput = context.InputAst.ToString(); var presentCommands = new Dictionary <string, int>(); var commandBasedPredictor = _commandBasedPredictor; var commandToRequestPrediction = _commandToRequestPrediction; var result = commandBasedPredictor?.Item2?.GetSuggestion(commandName, inputParameterSet, rawUserInput, presentCommands, suggestionFromPredictorCount, maxAllowedCommandDuplicate, cancellationToken); if ((result != null) && (result.Count > 0)) { var suggestionSource = SuggestionSource.PreviousCommand; if (string.Equals(commandToRequestPrediction, commandBasedPredictor?.Item1, StringComparison.Ordinal)) { suggestionSource = SuggestionSource.CurrentCommand; } for (var i = 0; i < result.Count; ++i) { result.UpdateSuggestionSource(i, suggestionSource); } } if ((result == null) || (result.Count < suggestionFromPredictorCount)) { var fallbackPredictor = _fallbackPredictor; var suggestionCountToRequest = (result == null) ? suggestionFromPredictorCount : suggestionFromPredictorCount - result.Count; var resultsFromFallback = fallbackPredictor?.GetSuggestion(commandName, inputParameterSet, rawUserInput, presentCommands, suggestionCountToRequest, maxAllowedCommandDuplicate, cancellationToken); if ((result == null) && (resultsFromFallback != null)) { result = resultsFromFallback; for (var i = 0; i < result.Count; ++i) { result.UpdateSuggestionSource(i, SuggestionSource.StaticCommands); } } else if ((resultsFromFallback != null) && (resultsFromFallback.Count > 0)) { for (var i = 0; i < resultsFromFallback.Count; ++i) { result.AddSuggestion(resultsFromFallback.PredictiveSuggestions[i], resultsFromFallback.SourceTexts[i], SuggestionSource.StaticCommands); } } } if (suggestionCount > 1) { // Add the survey/feedback cmdlet at the end if the user isn't typing it. bool isSurveyCmdletFound = false; if (result != null) { foreach (var predictiveCommand in result.SourceTexts) { if (string.Equals(predictiveCommand, _surveyCmdlets[_azContext.Cohort].Command, StringComparison.Ordinal)) { isSurveyCmdletFound = true; break; } } } else { result = new CommandLineSuggestion(); } if (!isSurveyCmdletFound) { var toAddCmdlet = _surveyCmdlets[_azContext.Cohort].Command; var toAddDescription = _surveyCmdlets[_azContext.Cohort].Description; result.AddSuggestion(new PredictiveSuggestion($"{toAddCmdlet} # {toAddDescription}", toAddCmdlet), toAddCmdlet, SuggestionSource.StaticCommands); } } return(result); }
/// <summary> /// Iterate over command elements to extract local parameter values. /// /// Store these values by a key /// consisting of the suffix of the command + the parameter name. There are some exceptions, e.g. /// credential, location, where the parameter name itself is the key. /// /// For example, New-AzResourceGroup -Name Hello -Location 'EastUS' will store into local parameters: /// ResourceGroupName => Hello /// Location => 'EastUS' /// </summary> /// <param name="command">The command ast element</param> /// <remarks> /// This doesn't support positional parameter. /// </remarks> private void ExtractLocalParameters(CommandAst command) { // Azure PowerShell command is in the form of {Verb}-Az{Noun}, e.g. New-AzResource. // We need to extract the noun to construct the parameter name. var commandName = command.GetCommandName(); var commandNoun = ParameterValuePredictor.GetAzCommandNoun(commandName)?.ToLower(); if (commandNoun == null) { return; } Dictionary <string, string> commandNounMap = null; _commandParamToResourceMap?.TryGetValue(commandNoun, out commandNounMap); bool isParameterUpdated = false; var parameterSet = new ParameterSet(command, _azContext); for (var i = 0; i < parameterSet.Parameters.Count; ++i) { var parameterName = parameterSet.Parameters[i].Name.ToLower(); var parameterValue = parameterSet.Parameters[i].Value; if (string.IsNullOrWhiteSpace(parameterValue) || string.IsNullOrWhiteSpace(parameterName)) { continue; } var parameterKey = parameterName; var mappedValue = parameterKey; if (commandNounMap?.TryGetValue(parameterName, out mappedValue) == true) { parameterKey = mappedValue; } this._localParameterValues.AddOrUpdate(parameterKey, parameterValue, (k, v) => parameterValue); isParameterUpdated = true; } if (isParameterUpdated) { _cancellationTokenSource?.Cancel(); _cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = _cancellationTokenSource.Token; Task.Run(() => { String localParameterValuesJson = JsonSerializer.Serialize <ConcurrentDictionary <string, string> >(_localParameterValues, JsonUtilities.DefaultSerializerOptions); _mutex.WaitOne(); cancellationToken.ThrowIfCancellationRequested(); try { System.IO.File.WriteAllText(_paramValueHistoryFilePath, localParameterValuesJson); } finally { _mutex.ReleaseMutex(); } }, cancellationToken); } }
/// <inheritdoc/> /// <remarks> /// Tries to get the suggestions for the user input from the command history. If that doesn't find /// <paramref name="suggestionCount"/> suggestions, it'll fallback to find the suggestion regardless of command history. /// </remarks> public CommandLineSuggestion GetSuggestion(Ast input, int suggestionCount, int maxAllowedCommandDuplicate, CancellationToken cancellationToken) { Validation.CheckArgument(input, $"{nameof(input)} cannot be null"); Validation.CheckArgument <ArgumentOutOfRangeException>(suggestionCount > 0, $"{nameof(suggestionCount)} must be larger than 0."); Validation.CheckArgument <ArgumentOutOfRangeException>(maxAllowedCommandDuplicate > 0, $"{nameof(maxAllowedCommandDuplicate)} must be larger than 0."); var commandAst = input.FindAll(p => p is CommandAst, true).LastOrDefault() as CommandAst; var commandName = (commandAst?.CommandElements?.FirstOrDefault() as StringConstantExpressionAst)?.Value; if (string.IsNullOrWhiteSpace(commandName)) { return(null); } var inputParameterSet = new ParameterSet(commandAst); var rawUserInput = input.Extent.Text; var presentCommands = new Dictionary <string, int>(); var commandBasedPredictor = _commandBasedPredictor; var commandToRequestPrediction = _commandToRequestPrediction; var result = commandBasedPredictor?.Item2?.GetSuggestion(commandName, inputParameterSet, rawUserInput, presentCommands, suggestionCount, maxAllowedCommandDuplicate, cancellationToken); if ((result != null) && (result.Count > 0)) { var suggestionSource = SuggestionSource.PreviousCommand; if (string.Equals(commandToRequestPrediction, commandBasedPredictor?.Item1, StringComparison.Ordinal)) { suggestionSource = SuggestionSource.CurrentCommand; } for (var i = 0; i < result.Count; ++i) { result.UpdateSuggestionSource(i, suggestionSource); } } if ((result == null) || (result.Count < suggestionCount)) { var fallbackPredictor = _fallbackPredictor; var suggestionCountToRequest = (result == null) ? suggestionCount : suggestionCount - result.Count; var resultsFromFallback = fallbackPredictor?.GetSuggestion(commandName, inputParameterSet, rawUserInput, presentCommands, suggestionCountToRequest, maxAllowedCommandDuplicate, cancellationToken); if ((result == null) && (resultsFromFallback != null)) { result = resultsFromFallback; for (var i = 0; i < result.Count; ++i) { result.UpdateSuggestionSource(i, SuggestionSource.StaticCommands); } } else if ((resultsFromFallback != null) && (resultsFromFallback.Count > 0)) { for (var i = 0; i < resultsFromFallback.Count; ++i) { if (result.SourceTexts.Contains(resultsFromFallback.SourceTexts[i])) { continue; } result.AddSuggestion(resultsFromFallback.PredictiveSuggestions[i], resultsFromFallback.SourceTexts[i], SuggestionSource.StaticCommands); } } } return(result); }