7712 lines
341 KiB
C#
7712 lines
341 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using System.Collections;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Data;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Management.Automation.Internal;
|
|
using System.Management.Automation.Language;
|
|
using System.Management.Automation.Runspaces;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
|
|
using Microsoft.Management.Infrastructure;
|
|
using Microsoft.Management.Infrastructure.Options;
|
|
using Microsoft.PowerShell;
|
|
using Microsoft.PowerShell.Cim;
|
|
using Microsoft.PowerShell.Commands;
|
|
using Microsoft.PowerShell.Commands.Internal.Format;
|
|
|
|
namespace System.Management.Automation
|
|
{
|
|
/// <summary>
|
|
/// </summary>
|
|
public static class CompletionCompleters
|
|
{
|
|
static CompletionCompleters()
|
|
{
|
|
AppDomain.CurrentDomain.AssemblyLoad += UpdateTypeCacheOnAssemblyLoad;
|
|
}
|
|
|
|
private static void UpdateTypeCacheOnAssemblyLoad(object sender, AssemblyLoadEventArgs args)
|
|
{
|
|
// Just null out the cache - we'll rebuild it the next time someone tries to complete a type.
|
|
// We could rebuild it now, but we could be loading multiple assemblies (e.g. dependent assemblies)
|
|
// and there is no sense in rebuilding anything until we're done loading all of the assemblies.
|
|
Interlocked.Exchange(ref s_typeCache, null);
|
|
}
|
|
|
|
#region Command Names
|
|
|
|
/// <summary>
|
|
/// </summary>
|
|
/// <param name="commandName"></param>
|
|
/// <returns></returns>
|
|
public static IEnumerable<CompletionResult> CompleteCommand(string commandName)
|
|
{
|
|
return CompleteCommand(commandName, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// </summary>
|
|
/// <param name="commandName"></param>
|
|
/// <param name="moduleName"></param>
|
|
/// <param name="commandTypes"></param>
|
|
/// <returns></returns>
|
|
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]
|
|
public static IEnumerable<CompletionResult> CompleteCommand(string commandName, string moduleName, CommandTypes commandTypes = CommandTypes.All)
|
|
{
|
|
var runspace = Runspace.DefaultRunspace;
|
|
if (runspace == null)
|
|
{
|
|
// No runspace, just return no results.
|
|
return CommandCompletion.EmptyCompletionResult;
|
|
}
|
|
|
|
var helper = new PowerShellExecutionHelper(PowerShell.Create(RunspaceMode.CurrentRunspace));
|
|
var executionContext = helper.CurrentPowerShell.Runspace.ExecutionContext;
|
|
return CompleteCommand(new CompletionContext { WordToComplete = commandName, Helper = helper, ExecutionContext = executionContext }, moduleName, commandTypes);
|
|
}
|
|
|
|
internal static List<CompletionResult> CompleteCommand(CompletionContext context)
|
|
{
|
|
return CompleteCommand(context, null);
|
|
}
|
|
|
|
private static List<CompletionResult> CompleteCommand(CompletionContext context, string moduleName, CommandTypes types = CommandTypes.All)
|
|
{
|
|
var addAmpersandIfNecessary = IsAmpersandNeeded(context, false);
|
|
|
|
string commandName = context.WordToComplete;
|
|
string quote = HandleDoubleAndSingleQuote(ref commandName);
|
|
|
|
List<CompletionResult> commandResults = null;
|
|
|
|
if (commandName.IndexOfAny(Utils.Separators.DirectoryOrDrive) == -1)
|
|
{
|
|
// The name to complete is neither module qualified nor is it a relative/rooted file path.
|
|
|
|
Ast lastAst = null;
|
|
if (context.RelatedAsts != null && context.RelatedAsts.Count > 0)
|
|
{
|
|
lastAst = context.RelatedAsts.Last();
|
|
}
|
|
|
|
commandResults = ExecuteGetCommandCommand(useModulePrefix: false);
|
|
|
|
if (lastAst != null)
|
|
{
|
|
// We need to add the wildcard to the end so the regex is built correctly.
|
|
commandName += "*";
|
|
|
|
// Search the asts for function definitions that we might be calling
|
|
var findFunctionsVisitor = new FindFunctionsVisitor();
|
|
while (lastAst.Parent != null)
|
|
{
|
|
lastAst = lastAst.Parent;
|
|
}
|
|
|
|
lastAst.Visit(findFunctionsVisitor);
|
|
|
|
WildcardPattern commandNamePattern = WildcardPattern.Get(commandName, WildcardOptions.IgnoreCase);
|
|
foreach (var defn in findFunctionsVisitor.FunctionDefinitions)
|
|
{
|
|
if (commandNamePattern.IsMatch(defn.Name)
|
|
&& !commandResults.Any(cr => cr.CompletionText.Equals(defn.Name, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
// Results found in the current script are prepended to show up at the top of the list.
|
|
commandResults.Insert(0, GetCommandNameCompletionResult(defn.Name, defn, addAmpersandIfNecessary, quote));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If there is a single \, we might be looking for a module/snapin qualified command
|
|
var indexOfFirstColon = commandName.IndexOf(':');
|
|
var indexOfFirstBackslash = commandName.IndexOf('\\');
|
|
if (indexOfFirstBackslash > 0 && (indexOfFirstBackslash < indexOfFirstColon || indexOfFirstColon == -1))
|
|
{
|
|
// First try the name before the backslash as a module name.
|
|
// Use the exact module name provided by the user
|
|
moduleName = commandName.Substring(0, indexOfFirstBackslash);
|
|
commandName = commandName.Substring(indexOfFirstBackslash + 1);
|
|
|
|
commandResults = ExecuteGetCommandCommand(useModulePrefix: true);
|
|
}
|
|
}
|
|
|
|
return commandResults;
|
|
|
|
List<CompletionResult> ExecuteGetCommandCommand(bool useModulePrefix)
|
|
{
|
|
var powershell = context.Helper
|
|
.AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand))
|
|
.AddParameter("All")
|
|
.AddParameter("Name", commandName + "*");
|
|
|
|
if (moduleName != null)
|
|
{
|
|
powershell.AddParameter("Module", moduleName);
|
|
}
|
|
|
|
if (!types.Equals(CommandTypes.All))
|
|
{
|
|
powershell.AddParameter("CommandType", types);
|
|
}
|
|
|
|
// Exception is ignored, the user simply does not get any completion results if the pipeline fails
|
|
Exception exceptionThrown;
|
|
var commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
|
|
if (commandInfos == null || commandInfos.Count == 0)
|
|
{
|
|
powershell.Commands.Clear();
|
|
powershell
|
|
.AddCommandWithPreferenceSetting("Get-Command", typeof(GetCommandCommand))
|
|
.AddParameter("All")
|
|
.AddParameter("Name", commandName)
|
|
.AddParameter("UseAbbreviationExpansion");
|
|
|
|
if (moduleName != null)
|
|
{
|
|
powershell.AddParameter("Module", moduleName);
|
|
}
|
|
|
|
if (!types.Equals(CommandTypes.All))
|
|
{
|
|
powershell.AddParameter("CommandType", types);
|
|
}
|
|
|
|
commandInfos = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
}
|
|
|
|
List<CompletionResult> completionResults = null;
|
|
|
|
if (commandInfos != null && commandInfos.Count > 1)
|
|
{
|
|
// OrderBy is using stable sorting
|
|
var sortedCommandInfos = commandInfos.OrderBy(static a => a, new CommandNameComparer());
|
|
completionResults = MakeCommandsUnique(sortedCommandInfos, useModulePrefix, addAmpersandIfNecessary, quote);
|
|
}
|
|
else
|
|
{
|
|
completionResults = MakeCommandsUnique(commandInfos, useModulePrefix, addAmpersandIfNecessary, quote);
|
|
}
|
|
|
|
return completionResults;
|
|
}
|
|
}
|
|
|
|
private static readonly HashSet<string> s_keywordsToExcludeFromAddingAmpersand
|
|
= new HashSet<string>(StringComparer.OrdinalIgnoreCase) { nameof(TokenKind.InlineScript), nameof(TokenKind.Configuration) };
|
|
|
|
internal static CompletionResult GetCommandNameCompletionResult(string name, object command, bool addAmpersandIfNecessary, string quote)
|
|
{
|
|
string syntax = name, listItem = name;
|
|
|
|
var commandInfo = command as CommandInfo;
|
|
if (commandInfo != null)
|
|
{
|
|
try
|
|
{
|
|
listItem = commandInfo.Name;
|
|
// This may require parsing a script, which could fail in a number of different ways
|
|
// (syntax errors, security exceptions, etc.) If so, the name is fine for the tooltip.
|
|
syntax = commandInfo.Syntax;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
syntax = string.IsNullOrEmpty(syntax) ? name : syntax;
|
|
bool needAmpersand;
|
|
|
|
if (CompletionRequiresQuotes(name, false))
|
|
{
|
|
needAmpersand = quote == string.Empty && addAmpersandIfNecessary;
|
|
string quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
{
|
|
name = name.Replace("'", "''");
|
|
}
|
|
else
|
|
{
|
|
name = name.Replace("`", "``");
|
|
name = name.Replace("$", "`$");
|
|
}
|
|
|
|
name = quoteInUse + name + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
needAmpersand = quote == string.Empty && addAmpersandIfNecessary &&
|
|
Tokenizer.IsKeyword(name) && !s_keywordsToExcludeFromAddingAmpersand.Contains(name);
|
|
name = quote + name + quote;
|
|
}
|
|
|
|
// It's useless to call ForEach-Object (foreach) as the first command of a pipeline. For example:
|
|
// PS C:\> fore<tab> ---> PS C:\> foreach (expected, use as the keyword)
|
|
// PS C:\> fore<tab> ---> PS C:\> & foreach (unexpected, ForEach-Object is seldom used as the first command of a pipeline)
|
|
if (needAmpersand && name != SpecialVariables.@foreach)
|
|
{
|
|
name = "& " + name;
|
|
}
|
|
|
|
return new CompletionResult(name, listItem, CompletionResultType.Command, syntax);
|
|
}
|
|
|
|
internal static List<CompletionResult> MakeCommandsUnique(IEnumerable<PSObject> commandInfoPsObjs, bool includeModulePrefix, bool addAmpersandIfNecessary, string quote)
|
|
{
|
|
List<CompletionResult> results = new List<CompletionResult>();
|
|
if (commandInfoPsObjs == null || !commandInfoPsObjs.Any())
|
|
{
|
|
return results;
|
|
}
|
|
|
|
var commandTable = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var psobj in commandInfoPsObjs)
|
|
{
|
|
object baseObj = PSObject.Base(psobj);
|
|
string name = null;
|
|
|
|
var commandInfo = baseObj as CommandInfo;
|
|
if (commandInfo != null)
|
|
{
|
|
// Skip the private commands
|
|
if (commandInfo.Visibility == SessionStateEntryVisibility.Private) { continue; }
|
|
|
|
name = commandInfo.Name;
|
|
if (includeModulePrefix && !string.IsNullOrEmpty(commandInfo.ModuleName))
|
|
{
|
|
// The command might be a prefixed commandInfo that we get by importing a module with the -Prefix parameter, for example:
|
|
// FooModule.psm1: Get-Foo
|
|
// import-module FooModule -Prefix PowerShell
|
|
// --> command 'Get-PowerShellFoo' in the global session state (prefixed commandInfo)
|
|
// command 'Get-Foo' in the module session state (un-prefixed commandInfo)
|
|
// in that case, we should not add the module name qualification because it doesn't work
|
|
if (string.IsNullOrEmpty(commandInfo.Prefix) || !ModuleCmdletBase.IsPrefixedCommand(commandInfo))
|
|
{
|
|
name = commandInfo.ModuleName + "\\" + commandInfo.Name;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
name = baseObj as string;
|
|
if (name == null) { continue; }
|
|
}
|
|
|
|
object value;
|
|
if (!commandTable.TryGetValue(name, out value))
|
|
{
|
|
commandTable.Add(name, baseObj);
|
|
}
|
|
else
|
|
{
|
|
var list = value as List<object>;
|
|
if (list != null)
|
|
{
|
|
list.Add(baseObj);
|
|
}
|
|
else
|
|
{
|
|
list = new List<object> { value, baseObj };
|
|
commandTable[name] = list;
|
|
}
|
|
}
|
|
}
|
|
|
|
List<CompletionResult> endResults = null;
|
|
foreach (var keyValuePair in commandTable)
|
|
{
|
|
var commandList = keyValuePair.Value as List<object>;
|
|
if (commandList != null)
|
|
{
|
|
if (endResults == null)
|
|
{
|
|
endResults = new List<CompletionResult>();
|
|
}
|
|
|
|
// The first command might be an un-prefixed commandInfo that we get by importing a module with the -Prefix parameter,
|
|
// in that case, we should add the module name qualification because if the module is not in the module path, calling
|
|
// 'Get-Foo' directly doesn't work
|
|
string completionName = keyValuePair.Key;
|
|
if (!includeModulePrefix)
|
|
{
|
|
var commandInfo = commandList[0] as CommandInfo;
|
|
if (commandInfo != null && !string.IsNullOrEmpty(commandInfo.Prefix))
|
|
{
|
|
Diagnostics.Assert(!string.IsNullOrEmpty(commandInfo.ModuleName), "the module name should exist if commandInfo.Prefix is not an empty string");
|
|
if (!ModuleCmdletBase.IsPrefixedCommand(commandInfo))
|
|
{
|
|
completionName = commandInfo.ModuleName + "\\" + completionName;
|
|
}
|
|
}
|
|
}
|
|
|
|
results.Add(GetCommandNameCompletionResult(completionName, commandList[0], addAmpersandIfNecessary, quote));
|
|
|
|
// For the other commands that are hidden, we need to disambiguate,
|
|
// but put these at the end as it's less likely any of the hidden
|
|
// commands are desired. If we can't add anything to disambiguate,
|
|
// then we'll skip adding a completion result.
|
|
for (int index = 1; index < commandList.Count; index++)
|
|
{
|
|
var commandInfo = commandList[index] as CommandInfo;
|
|
Diagnostics.Assert(commandInfo != null, "Elements should always be CommandInfo");
|
|
|
|
if (commandInfo.CommandType == CommandTypes.Application)
|
|
{
|
|
endResults.Add(GetCommandNameCompletionResult(commandInfo.Definition, commandInfo, addAmpersandIfNecessary, quote));
|
|
}
|
|
else if (!string.IsNullOrEmpty(commandInfo.ModuleName))
|
|
{
|
|
var name = commandInfo.ModuleName + "\\" + commandInfo.Name;
|
|
endResults.Add(GetCommandNameCompletionResult(name, commandInfo, addAmpersandIfNecessary, quote));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The first command might be an un-prefixed commandInfo that we get by importing a module with the -Prefix parameter,
|
|
// in that case, we should add the module name qualification because if the module is not in the module path, calling
|
|
// 'Get-Foo' directly doesn't work
|
|
string completionName = keyValuePair.Key;
|
|
if (!includeModulePrefix)
|
|
{
|
|
var commandInfo = keyValuePair.Value as CommandInfo;
|
|
if (commandInfo != null && !string.IsNullOrEmpty(commandInfo.Prefix))
|
|
{
|
|
Diagnostics.Assert(!string.IsNullOrEmpty(commandInfo.ModuleName), "the module name should exist if commandInfo.Prefix is not an empty string");
|
|
if (!ModuleCmdletBase.IsPrefixedCommand(commandInfo))
|
|
{
|
|
completionName = commandInfo.ModuleName + "\\" + completionName;
|
|
}
|
|
}
|
|
}
|
|
|
|
results.Add(GetCommandNameCompletionResult(completionName, keyValuePair.Value, addAmpersandIfNecessary, quote));
|
|
}
|
|
}
|
|
|
|
if (endResults != null && endResults.Count > 0)
|
|
{
|
|
results.AddRange(endResults);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private sealed class FindFunctionsVisitor : AstVisitor
|
|
{
|
|
internal readonly List<FunctionDefinitionAst> FunctionDefinitions = new List<FunctionDefinitionAst>();
|
|
|
|
public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst)
|
|
{
|
|
FunctionDefinitions.Add(functionDefinitionAst);
|
|
return AstVisitAction.Continue;
|
|
}
|
|
}
|
|
|
|
#endregion Command Names
|
|
|
|
#region Module Names
|
|
|
|
internal static List<CompletionResult> CompleteModuleName(CompletionContext context, bool loadedModulesOnly, bool skipEditionCheck = false)
|
|
{
|
|
var moduleName = context.WordToComplete ?? string.Empty;
|
|
var result = new List<CompletionResult>();
|
|
var quote = HandleDoubleAndSingleQuote(ref moduleName);
|
|
|
|
if (!moduleName.EndsWith('*'))
|
|
{
|
|
moduleName += "*";
|
|
}
|
|
|
|
var powershell = context.Helper.AddCommandWithPreferenceSetting("Get-Module", typeof(GetModuleCommand)).AddParameter("Name", moduleName);
|
|
if (!loadedModulesOnly)
|
|
{
|
|
powershell.AddParameter("ListAvailable", true);
|
|
|
|
// -SkipEditionCheck should only be set or apply to -ListAvailable
|
|
if (skipEditionCheck)
|
|
{
|
|
powershell.AddParameter("SkipEditionCheck", true);
|
|
}
|
|
}
|
|
|
|
Exception exceptionThrown;
|
|
var psObjects = context.Helper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
|
|
if (psObjects != null)
|
|
{
|
|
foreach (dynamic moduleInfo in psObjects)
|
|
{
|
|
var completionText = moduleInfo.Name.ToString();
|
|
var listItemText = completionText;
|
|
var toolTip = "Description: " + moduleInfo.Description.ToString() + "\r\nModuleType: "
|
|
+ moduleInfo.ModuleType.ToString() + "\r\nPath: "
|
|
+ moduleInfo.Path.ToString();
|
|
|
|
if (CompletionRequiresQuotes(completionText, false))
|
|
{
|
|
var quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
completionText = quote + completionText + quote;
|
|
}
|
|
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, toolTip));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
#endregion Module Names
|
|
|
|
#region Command Parameters
|
|
private static readonly string[] s_parameterNamesOfImportDSCResource = { "Name", "ModuleName", "ModuleVersion" };
|
|
|
|
internal static List<CompletionResult> CompleteCommandParameter(CompletionContext context)
|
|
{
|
|
string partialName = null;
|
|
bool withColon = false;
|
|
CommandAst commandAst = null;
|
|
List<CompletionResult> result = new List<CompletionResult>();
|
|
|
|
// Find the parameter ast, it will be near or at the end
|
|
CommandParameterAst parameterAst = null;
|
|
DynamicKeywordStatementAst keywordAst = null;
|
|
for (int i = context.RelatedAsts.Count - 1; i >= 0; i--)
|
|
{
|
|
if (keywordAst == null)
|
|
keywordAst = context.RelatedAsts[i] as DynamicKeywordStatementAst;
|
|
parameterAst = (context.RelatedAsts[i] as CommandParameterAst);
|
|
if (parameterAst != null) break;
|
|
}
|
|
|
|
if (parameterAst != null)
|
|
{
|
|
keywordAst = parameterAst.Parent as DynamicKeywordStatementAst;
|
|
}
|
|
|
|
// If parent is DynamicKeywordStatementAst - 'Import-DscResource',
|
|
// then customize the auto completion results
|
|
if (keywordAst != null && string.Equals(keywordAst.Keyword.Keyword, "Import-DscResource", StringComparison.OrdinalIgnoreCase)
|
|
&& !string.IsNullOrWhiteSpace(context.WordToComplete) && context.WordToComplete.StartsWith('-'))
|
|
{
|
|
var lastAst = context.RelatedAsts.Last();
|
|
var wordToMatch = string.Concat(context.WordToComplete.AsSpan(1), "*");
|
|
var pattern = WildcardPattern.Get(wordToMatch, WildcardOptions.IgnoreCase);
|
|
var parameterNames = keywordAst.CommandElements.Where(static ast => ast is CommandParameterAst).Select(static ast => (ast as CommandParameterAst).ParameterName);
|
|
foreach (var parameterName in s_parameterNamesOfImportDSCResource)
|
|
{
|
|
if (pattern.IsMatch(parameterName) && !parameterNames.Contains(parameterName, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
string tooltip = "[String] " + parameterName;
|
|
result.Add(new CompletionResult("-" + parameterName, parameterName, CompletionResultType.ParameterName, tooltip));
|
|
}
|
|
}
|
|
|
|
if (result.Count > 0)
|
|
{
|
|
context.ReplacementLength = context.WordToComplete.Length;
|
|
context.ReplacementIndex = lastAst.Extent.StartOffset;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
if (parameterAst != null)
|
|
{
|
|
// Parent must be a command
|
|
commandAst = (CommandAst)parameterAst.Parent;
|
|
partialName = parameterAst.ParameterName;
|
|
withColon = context.WordToComplete.EndsWith(':');
|
|
}
|
|
else
|
|
{
|
|
// No CommandParameterAst is found. It could be a StringConstantExpressionAst "-"
|
|
if (!(context.RelatedAsts[context.RelatedAsts.Count - 1] is StringConstantExpressionAst dashAst))
|
|
return result;
|
|
if (!dashAst.Value.Trim().Equals("-", StringComparison.OrdinalIgnoreCase))
|
|
return result;
|
|
|
|
// Parent must be a command
|
|
commandAst = (CommandAst)dashAst.Parent;
|
|
partialName = string.Empty;
|
|
}
|
|
|
|
PseudoBindingInfo pseudoBinding = new PseudoParameterBinder()
|
|
.DoPseudoParameterBinding(commandAst, null, parameterAst, PseudoParameterBinder.BindingType.ParameterCompletion);
|
|
// The command cannot be found or it's not a cmdlet, not a script cmdlet, not a function.
|
|
// Try completing as if it the parameter is a command argument for native command completion.
|
|
if (pseudoBinding == null)
|
|
{
|
|
return CompleteCommandArgument(context);
|
|
}
|
|
|
|
switch (pseudoBinding.InfoType)
|
|
{
|
|
case PseudoBindingInfoType.PseudoBindingFail:
|
|
// The command is a cmdlet or script cmdlet. Binding failed
|
|
result = GetParameterCompletionResults(partialName, uint.MaxValue, pseudoBinding.UnboundParameters, withColon);
|
|
break;
|
|
case PseudoBindingInfoType.PseudoBindingSucceed:
|
|
// The command is a cmdlet or script cmdlet. Binding succeeded.
|
|
result = GetParameterCompletionResults(partialName, pseudoBinding, parameterAst, withColon);
|
|
break;
|
|
}
|
|
|
|
if (result.Count == 0)
|
|
{
|
|
result = pseudoBinding.CommandName.Equals("Set-Location", StringComparison.OrdinalIgnoreCase)
|
|
? new List<CompletionResult>(CompleteFilename(context, containerOnly: true, extension: null))
|
|
: new List<CompletionResult>(CompleteFilename(context));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the parameter completion results when the pseudo binding was successful.
|
|
/// </summary>
|
|
/// <param name="parameterName"></param>
|
|
/// <param name="bindingInfo"></param>
|
|
/// <param name="parameterAst"></param>
|
|
/// <param name="withColon"></param>
|
|
/// <returns></returns>
|
|
private static List<CompletionResult> GetParameterCompletionResults(string parameterName, PseudoBindingInfo bindingInfo, CommandParameterAst parameterAst, bool withColon)
|
|
{
|
|
Diagnostics.Assert(bindingInfo.InfoType.Equals(PseudoBindingInfoType.PseudoBindingSucceed), "The pseudo binding should succeed");
|
|
List<CompletionResult> result = new List<CompletionResult>();
|
|
|
|
if (parameterName == string.Empty)
|
|
{
|
|
result = GetParameterCompletionResults(
|
|
parameterName,
|
|
bindingInfo.ValidParameterSetsFlags,
|
|
bindingInfo.UnboundParameters,
|
|
withColon);
|
|
return result;
|
|
}
|
|
|
|
if (bindingInfo.ParametersNotFound.Count > 0)
|
|
{
|
|
// The parameter name cannot be matched to any parameter
|
|
if (bindingInfo.ParametersNotFound.Any(pAst => parameterAst.GetHashCode() == pAst.GetHashCode()))
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
|
|
if (bindingInfo.AmbiguousParameters.Count > 0)
|
|
{
|
|
// The parameter name is ambiguous. It's ignored in the pseudo binding, and we should search in the UnboundParameters
|
|
if (bindingInfo.AmbiguousParameters.Any(pAst => parameterAst.GetHashCode() == pAst.GetHashCode()))
|
|
{
|
|
result = GetParameterCompletionResults(
|
|
parameterName,
|
|
bindingInfo.ValidParameterSetsFlags,
|
|
bindingInfo.UnboundParameters,
|
|
withColon);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
if (bindingInfo.DuplicateParameters.Count > 0)
|
|
{
|
|
// The parameter name is resolved to a parameter that is already bound. We search it in the BoundParameters
|
|
if (bindingInfo.DuplicateParameters.Any(pAst => parameterAst.GetHashCode() == pAst.Parameter.GetHashCode()))
|
|
{
|
|
result = GetParameterCompletionResults(
|
|
parameterName,
|
|
bindingInfo.ValidParameterSetsFlags,
|
|
bindingInfo.BoundParameters.Values,
|
|
withColon);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// The parameter should be bound in the pseudo binding during the named binding
|
|
string matchedParameterName = null;
|
|
foreach (KeyValuePair<string, AstParameterArgumentPair> entry in bindingInfo.BoundArguments)
|
|
{
|
|
switch (entry.Value.ParameterArgumentType)
|
|
{
|
|
case AstParameterArgumentType.AstPair:
|
|
{
|
|
AstPair pair = (AstPair)entry.Value;
|
|
if (pair.ParameterSpecified && pair.Parameter.GetHashCode() == parameterAst.GetHashCode())
|
|
{
|
|
matchedParameterName = entry.Key;
|
|
}
|
|
else if (pair.ArgumentIsCommandParameterAst && pair.Argument.GetHashCode() == parameterAst.GetHashCode())
|
|
{
|
|
// The parameter name cannot be resolved to a parameter
|
|
return result;
|
|
}
|
|
}
|
|
|
|
break;
|
|
case AstParameterArgumentType.Fake:
|
|
{
|
|
FakePair pair = (FakePair)entry.Value;
|
|
if (pair.ParameterSpecified && pair.Parameter.GetHashCode() == parameterAst.GetHashCode())
|
|
{
|
|
matchedParameterName = entry.Key;
|
|
}
|
|
}
|
|
|
|
break;
|
|
case AstParameterArgumentType.Switch:
|
|
{
|
|
SwitchPair pair = (SwitchPair)entry.Value;
|
|
if (pair.ParameterSpecified && pair.Parameter.GetHashCode() == parameterAst.GetHashCode())
|
|
{
|
|
matchedParameterName = entry.Key;
|
|
}
|
|
}
|
|
|
|
break;
|
|
case AstParameterArgumentType.AstArray:
|
|
case AstParameterArgumentType.PipeObject:
|
|
break;
|
|
}
|
|
|
|
if (matchedParameterName != null)
|
|
break;
|
|
}
|
|
|
|
Diagnostics.Assert(matchedParameterName != null, "we should find matchedParameterName from the BoundArguments");
|
|
MergedCompiledCommandParameter param = bindingInfo.BoundParameters[matchedParameterName];
|
|
|
|
WildcardPattern pattern = WildcardPattern.Get(parameterName + "*", WildcardOptions.IgnoreCase);
|
|
string parameterType = "[" + ToStringCodeMethods.Type(param.Parameter.Type, dropNamespaces: true) + "] ";
|
|
string colonSuffix = withColon ? ":" : string.Empty;
|
|
if (pattern.IsMatch(matchedParameterName))
|
|
{
|
|
string completionText = "-" + matchedParameterName + colonSuffix;
|
|
string tooltip = parameterType + matchedParameterName;
|
|
result.Add(new CompletionResult(completionText, matchedParameterName, CompletionResultType.ParameterName, tooltip));
|
|
}
|
|
|
|
// Process alias when there is partial input
|
|
result.AddRange(from alias in param.Parameter.Aliases
|
|
where pattern.IsMatch(alias)
|
|
select
|
|
new CompletionResult("-" + alias + colonSuffix, alias, CompletionResultType.ParameterName,
|
|
parameterType + alias));
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the parameter completion results by using the given valid parameter sets and available parameters.
|
|
/// </summary>
|
|
/// <param name="parameterName"></param>
|
|
/// <param name="validParameterSetFlags"></param>
|
|
/// <param name="parameters"></param>
|
|
/// <param name="withColon"></param>
|
|
/// <returns></returns>
|
|
private static List<CompletionResult> GetParameterCompletionResults(
|
|
string parameterName,
|
|
uint validParameterSetFlags,
|
|
IEnumerable<MergedCompiledCommandParameter> parameters,
|
|
bool withColon)
|
|
{
|
|
var result = new List<CompletionResult>();
|
|
var commonParamResult = new List<CompletionResult>();
|
|
var pattern = WildcardPattern.Get(parameterName + "*", WildcardOptions.IgnoreCase);
|
|
var colonSuffix = withColon ? ":" : string.Empty;
|
|
|
|
bool addCommonParameters = true;
|
|
foreach (MergedCompiledCommandParameter param in parameters)
|
|
{
|
|
bool inParameterSet = (param.Parameter.ParameterSetFlags & validParameterSetFlags) != 0 || param.Parameter.IsInAllSets;
|
|
if (!inParameterSet)
|
|
continue;
|
|
|
|
string name = param.Parameter.Name;
|
|
string type = "[" + ToStringCodeMethods.Type(param.Parameter.Type, dropNamespaces: true) + "] ";
|
|
bool isCommonParameter = Cmdlet.CommonParameters.Contains(name, StringComparer.OrdinalIgnoreCase);
|
|
List<CompletionResult> listInUse = isCommonParameter ? commonParamResult : result;
|
|
|
|
if (pattern.IsMatch(name))
|
|
{
|
|
// Then using functions to back dynamic keywords, we don't necessarily
|
|
// want all of the parameters to be shown to the user. Those that are marked
|
|
// DontShow will not be displayed. Also, if any of the parameters have
|
|
// don't show set, we won't show any of the common parameters either.
|
|
bool showToUser = true;
|
|
var compiledAttributes = param.Parameter.CompiledAttributes;
|
|
if (compiledAttributes != null && compiledAttributes.Count > 0)
|
|
{
|
|
foreach (var attr in compiledAttributes)
|
|
{
|
|
var pattr = attr as ParameterAttribute;
|
|
if (pattr != null && pattr.DontShow)
|
|
{
|
|
showToUser = false;
|
|
addCommonParameters = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (showToUser)
|
|
{
|
|
string completionText = "-" + name + colonSuffix;
|
|
string tooltip = type + name;
|
|
listInUse.Add(new CompletionResult(completionText, name, CompletionResultType.ParameterName,
|
|
tooltip));
|
|
}
|
|
}
|
|
|
|
if (parameterName != string.Empty)
|
|
{
|
|
// Process alias when there is partial input
|
|
listInUse.AddRange(from alias in param.Parameter.Aliases
|
|
where pattern.IsMatch(alias)
|
|
select
|
|
new CompletionResult("-" + alias + colonSuffix, alias, CompletionResultType.ParameterName,
|
|
type + alias));
|
|
}
|
|
}
|
|
|
|
// Add the common parameters to the results if expected.
|
|
if (addCommonParameters)
|
|
{
|
|
result.AddRange(commonParamResult);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get completion results for operators that start with <paramref name="wordToComplete"/>
|
|
/// </summary>
|
|
/// <param name="wordToComplete">The starting text of the operator to complete.</param>
|
|
/// <returns>A list of completion results.</returns>
|
|
public static List<CompletionResult> CompleteOperator(string wordToComplete)
|
|
{
|
|
if (wordToComplete.StartsWith('-'))
|
|
{
|
|
wordToComplete = wordToComplete.Substring(1);
|
|
}
|
|
|
|
return (from op in Tokenizer._operatorText
|
|
where op.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)
|
|
orderby op
|
|
select new CompletionResult("-" + op, op, CompletionResultType.ParameterName, GetOperatorDescription(op))).ToList();
|
|
}
|
|
|
|
private static string GetOperatorDescription(string op)
|
|
{
|
|
return ResourceManagerCache.GetResourceString(typeof(CompletionCompleters).Assembly,
|
|
"System.Management.Automation.resources.TabCompletionStrings",
|
|
op + "OperatorDescription");
|
|
}
|
|
|
|
#endregion Command Parameters
|
|
|
|
#region Command Arguments
|
|
|
|
internal static List<CompletionResult> CompleteCommandArgument(CompletionContext context)
|
|
{
|
|
CommandAst commandAst = null;
|
|
List<CompletionResult> result = new List<CompletionResult>();
|
|
|
|
// Find the expression ast. It should be at the end if there is one
|
|
ExpressionAst expressionAst = null;
|
|
MemberExpressionAst secondToLastMemberAst = null;
|
|
Ast lastAst = context.RelatedAsts.Last();
|
|
|
|
expressionAst = lastAst as ExpressionAst;
|
|
if (expressionAst != null)
|
|
{
|
|
if (expressionAst.Parent is CommandAst)
|
|
{
|
|
commandAst = (CommandAst)expressionAst.Parent;
|
|
|
|
if (expressionAst is ErrorExpressionAst && expressionAst.Extent.Text.EndsWith(','))
|
|
{
|
|
context.WordToComplete = string.Empty;
|
|
// BUGBUG context.CursorPosition = expressionAst.Extent.StartScriptPosition;
|
|
}
|
|
else if (commandAst.CommandElements.Count == 1 || context.WordToComplete == string.Empty)
|
|
{
|
|
expressionAst = null;
|
|
}
|
|
else if (commandAst.CommandElements.Count > 2)
|
|
{
|
|
var length = commandAst.CommandElements.Count;
|
|
var index = 1;
|
|
|
|
for (; index < length; index++)
|
|
{
|
|
if (commandAst.CommandElements[index] == expressionAst)
|
|
break;
|
|
}
|
|
|
|
CommandElementAst secondToLastAst = null;
|
|
if (index > 1)
|
|
{
|
|
secondToLastAst = commandAst.CommandElements[index - 1];
|
|
secondToLastMemberAst = secondToLastAst as MemberExpressionAst;
|
|
}
|
|
|
|
var partialPathAst = expressionAst as StringConstantExpressionAst;
|
|
if (partialPathAst != null && secondToLastAst != null &&
|
|
partialPathAst.StringConstantType == StringConstantType.BareWord &&
|
|
secondToLastAst.Extent.EndLineNumber == partialPathAst.Extent.StartLineNumber &&
|
|
secondToLastAst.Extent.EndColumnNumber == partialPathAst.Extent.StartColumnNumber &&
|
|
partialPathAst.Value.IndexOfAny(Utils.Separators.Directory) == 0)
|
|
{
|
|
var secondToLastStringConstantAst = secondToLastAst as StringConstantExpressionAst;
|
|
var secondToLastExpandableStringAst = secondToLastAst as ExpandableStringExpressionAst;
|
|
var secondToLastArrayAst = secondToLastAst as ArrayLiteralAst;
|
|
var secondToLastParamAst = secondToLastAst as CommandParameterAst;
|
|
|
|
if (secondToLastStringConstantAst != null || secondToLastExpandableStringAst != null)
|
|
{
|
|
var fullPath = ConcatenateStringPathArguments(secondToLastAst, partialPathAst.Value, context);
|
|
expressionAst = secondToLastStringConstantAst != null
|
|
? (ExpressionAst)secondToLastStringConstantAst
|
|
: (ExpressionAst)secondToLastExpandableStringAst;
|
|
|
|
context.ReplacementIndex = ((InternalScriptPosition)secondToLastAst.Extent.StartScriptPosition).Offset;
|
|
context.ReplacementLength += ((InternalScriptPosition)secondToLastAst.Extent.EndScriptPosition).Offset - context.ReplacementIndex;
|
|
context.WordToComplete = fullPath;
|
|
// context.CursorPosition = secondToLastAst.Extent.StartScriptPosition;
|
|
}
|
|
else if (secondToLastArrayAst != null)
|
|
{
|
|
// Handle cases like: dir -Path .\cd, 'a b'\new<tab>
|
|
var lastArrayElement = secondToLastArrayAst.Elements.LastOrDefault();
|
|
var fullPath = ConcatenateStringPathArguments(lastArrayElement, partialPathAst.Value, context);
|
|
if (fullPath != null)
|
|
{
|
|
expressionAst = secondToLastArrayAst;
|
|
|
|
context.ReplacementIndex = ((InternalScriptPosition)lastArrayElement.Extent.StartScriptPosition).Offset;
|
|
context.ReplacementLength += ((InternalScriptPosition)lastArrayElement.Extent.EndScriptPosition).Offset - context.ReplacementIndex;
|
|
context.WordToComplete = fullPath;
|
|
}
|
|
}
|
|
else if (secondToLastParamAst != null)
|
|
{
|
|
// Handle cases like: dir -Path: .\cd, 'a b'\new<tab> || dir -Path: 'a b'\new<tab>
|
|
var fullPath = ConcatenateStringPathArguments(secondToLastParamAst.Argument, partialPathAst.Value, context);
|
|
if (fullPath != null)
|
|
{
|
|
expressionAst = secondToLastParamAst.Argument;
|
|
|
|
context.ReplacementIndex = ((InternalScriptPosition)secondToLastParamAst.Argument.Extent.StartScriptPosition).Offset;
|
|
context.ReplacementLength += ((InternalScriptPosition)secondToLastParamAst.Argument.Extent.EndScriptPosition).Offset - context.ReplacementIndex;
|
|
context.WordToComplete = fullPath;
|
|
}
|
|
else
|
|
{
|
|
var arrayArgAst = secondToLastParamAst.Argument as ArrayLiteralAst;
|
|
if (arrayArgAst != null)
|
|
{
|
|
var lastArrayElement = arrayArgAst.Elements.LastOrDefault();
|
|
fullPath = ConcatenateStringPathArguments(lastArrayElement, partialPathAst.Value, context);
|
|
if (fullPath != null)
|
|
{
|
|
expressionAst = arrayArgAst;
|
|
|
|
context.ReplacementIndex = ((InternalScriptPosition)lastArrayElement.Extent.StartScriptPosition).Offset;
|
|
context.ReplacementLength += ((InternalScriptPosition)lastArrayElement.Extent.EndScriptPosition).Offset - context.ReplacementIndex;
|
|
context.WordToComplete = fullPath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (expressionAst.Parent is ArrayLiteralAst && expressionAst.Parent.Parent is CommandAst)
|
|
{
|
|
commandAst = (CommandAst)expressionAst.Parent.Parent;
|
|
|
|
if (commandAst.CommandElements.Count == 1 || context.WordToComplete == string.Empty)
|
|
{
|
|
// dir -Path a.txt, b.txt <tab>
|
|
expressionAst = null;
|
|
}
|
|
else
|
|
{
|
|
// dir -Path a.txt, b.txt c<tab>
|
|
expressionAst = (ExpressionAst)expressionAst.Parent;
|
|
}
|
|
}
|
|
else if (expressionAst.Parent is ArrayLiteralAst && expressionAst.Parent.Parent is CommandParameterAst)
|
|
{
|
|
// Handle scenarios such as
|
|
// dir -Path: a.txt, <tab> || dir -Path: a.txt, b.txt <tab>
|
|
commandAst = (CommandAst)expressionAst.Parent.Parent.Parent;
|
|
if (context.WordToComplete == string.Empty)
|
|
{
|
|
// dir -Path: a.txt, b.txt <tab>
|
|
expressionAst = null;
|
|
}
|
|
else
|
|
{
|
|
// dir -Path: a.txt, b<tab>
|
|
expressionAst = (ExpressionAst)expressionAst.Parent;
|
|
}
|
|
}
|
|
else if (expressionAst.Parent is CommandParameterAst && expressionAst.Parent.Parent is CommandAst)
|
|
{
|
|
commandAst = (CommandAst)expressionAst.Parent.Parent;
|
|
if (expressionAst is ErrorExpressionAst && expressionAst.Extent.Text.EndsWith(','))
|
|
{
|
|
// dir -Path: a.txt,<tab>
|
|
context.WordToComplete = string.Empty;
|
|
// context.CursorPosition = expressionAst.Extent.StartScriptPosition;
|
|
}
|
|
else if (context.WordToComplete == string.Empty)
|
|
{
|
|
// Handle scenario like this: Set-ExecutionPolicy -Scope:CurrentUser <tab>
|
|
expressionAst = null;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var paramAst = lastAst as CommandParameterAst;
|
|
if (paramAst != null)
|
|
{
|
|
commandAst = paramAst.Parent as CommandAst;
|
|
}
|
|
else
|
|
{
|
|
commandAst = lastAst as CommandAst;
|
|
}
|
|
}
|
|
|
|
if (commandAst == null)
|
|
{
|
|
// We don't know if this could be expanded into anything interesting
|
|
return result;
|
|
}
|
|
|
|
PseudoBindingInfo pseudoBinding = new PseudoParameterBinder()
|
|
.DoPseudoParameterBinding(commandAst, null, null, PseudoParameterBinder.BindingType.ArgumentCompletion);
|
|
|
|
do
|
|
{
|
|
// The command cannot be found, or it's NOT a cmdlet, NOT a script cmdlet and NOT a function
|
|
if (pseudoBinding == null)
|
|
break;
|
|
|
|
bool parsedArgumentsProvidesMatch = false;
|
|
|
|
if (pseudoBinding.AllParsedArguments != null && pseudoBinding.AllParsedArguments.Count > 0)
|
|
{
|
|
ArgumentLocation argLocation;
|
|
bool treatAsExpression = false;
|
|
|
|
if (expressionAst != null)
|
|
{
|
|
treatAsExpression = true;
|
|
var dashExp = expressionAst as StringConstantExpressionAst;
|
|
if (dashExp != null && dashExp.Value.Trim().Equals("-", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// "-" is represented as StringConstantExpressionAst. Most likely the user is typing a <tab>
|
|
// after it, so in the pseudo binder, we ignore it to avoid treating it as an argument.
|
|
// for example:
|
|
// Get-Content -Path "-<tab> --> Get-Content -Path ".\-patt.txt"
|
|
treatAsExpression = false;
|
|
}
|
|
}
|
|
|
|
if (treatAsExpression)
|
|
{
|
|
argLocation = FindTargetArgumentLocation(
|
|
pseudoBinding.AllParsedArguments, expressionAst);
|
|
}
|
|
else
|
|
{
|
|
argLocation = FindTargetArgumentLocation(
|
|
pseudoBinding.AllParsedArguments, context.TokenAtCursor ?? context.TokenBeforeCursor);
|
|
}
|
|
|
|
if (argLocation != null)
|
|
{
|
|
context.PseudoBindingInfo = pseudoBinding;
|
|
switch (pseudoBinding.InfoType)
|
|
{
|
|
case PseudoBindingInfoType.PseudoBindingSucceed:
|
|
result = GetArgumentCompletionResultsWithSuccessfulPseudoBinding(context, argLocation, commandAst);
|
|
break;
|
|
case PseudoBindingInfoType.PseudoBindingFail:
|
|
result = GetArgumentCompletionResultsWithFailedPseudoBinding(context, argLocation, commandAst);
|
|
break;
|
|
}
|
|
|
|
parsedArgumentsProvidesMatch = true;
|
|
}
|
|
}
|
|
|
|
if (!parsedArgumentsProvidesMatch)
|
|
{
|
|
int index = 0;
|
|
CommandElementAst prevElem = null;
|
|
if (expressionAst != null)
|
|
{
|
|
foreach (CommandElementAst eleAst in commandAst.CommandElements)
|
|
{
|
|
if (eleAst.GetHashCode() == expressionAst.GetHashCode())
|
|
break;
|
|
prevElem = eleAst;
|
|
index++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var token = context.TokenAtCursor ?? context.TokenBeforeCursor;
|
|
foreach (CommandElementAst eleAst in commandAst.CommandElements)
|
|
{
|
|
if (eleAst.Extent.StartOffset > token.Extent.EndOffset)
|
|
break;
|
|
prevElem = eleAst;
|
|
index++;
|
|
}
|
|
}
|
|
|
|
// positional argument with position 0
|
|
if (index == 1)
|
|
{
|
|
CompletePositionalArgument(
|
|
pseudoBinding.CommandName,
|
|
commandAst,
|
|
context,
|
|
result,
|
|
pseudoBinding.UnboundParameters,
|
|
pseudoBinding.DefaultParameterSetFlag,
|
|
uint.MaxValue,
|
|
0);
|
|
}
|
|
else
|
|
{
|
|
if (prevElem is CommandParameterAst && ((CommandParameterAst)prevElem).Argument == null)
|
|
{
|
|
var paramName = ((CommandParameterAst)prevElem).ParameterName;
|
|
var pattern = WildcardPattern.Get(paramName + "*", WildcardOptions.IgnoreCase);
|
|
foreach (MergedCompiledCommandParameter param in pseudoBinding.UnboundParameters)
|
|
{
|
|
if (pattern.IsMatch(param.Parameter.Name))
|
|
{
|
|
ProcessParameter(pseudoBinding.CommandName, commandAst, context, result, param);
|
|
break;
|
|
}
|
|
|
|
var isAliasMatch = false;
|
|
foreach (string alias in param.Parameter.Aliases)
|
|
{
|
|
if (pattern.IsMatch(alias))
|
|
{
|
|
isAliasMatch = true;
|
|
ProcessParameter(pseudoBinding.CommandName, commandAst, context, result, param);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isAliasMatch)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} while (false);
|
|
|
|
// Indicate if the current argument completion falls into those pre-defined cases and
|
|
// has been processed already.
|
|
bool hasBeenProcessed = false;
|
|
if (result.Count > 0 && result[result.Count - 1].Equals(CompletionResult.Null))
|
|
{
|
|
result.RemoveAt(result.Count - 1);
|
|
hasBeenProcessed = true;
|
|
|
|
if (result.Count > 0)
|
|
return result;
|
|
}
|
|
|
|
// Handle some special cases such as:
|
|
// & "get-comm<tab> --> & "Get-Command"
|
|
// & "sa<tab> --> & ".\sa[v].txt"
|
|
if (expressionAst == null && !hasBeenProcessed &&
|
|
commandAst.CommandElements.Count == 1 &&
|
|
commandAst.InvocationOperator != TokenKind.Unknown &&
|
|
context.WordToComplete != string.Empty)
|
|
{
|
|
// Use literal path after Ampersand
|
|
var tryCmdletCompletion = false;
|
|
var clearLiteralPathsKey = TurnOnLiteralPathOption(context);
|
|
|
|
if (context.WordToComplete.Contains('-'))
|
|
{
|
|
tryCmdletCompletion = true;
|
|
}
|
|
|
|
try
|
|
{
|
|
var fileCompletionResults = new List<CompletionResult>(CompleteFilename(context));
|
|
if (tryCmdletCompletion)
|
|
{
|
|
// It's actually command name completion, other than argument completion
|
|
var cmdletCompletionResults = CompleteCommand(context);
|
|
if (cmdletCompletionResults != null && cmdletCompletionResults.Count > 0)
|
|
{
|
|
fileCompletionResults.AddRange(cmdletCompletionResults);
|
|
}
|
|
}
|
|
|
|
return fileCompletionResults;
|
|
}
|
|
finally
|
|
{
|
|
if (clearLiteralPathsKey)
|
|
context.Options.Remove("LiteralPaths");
|
|
}
|
|
}
|
|
|
|
if (expressionAst is StringConstantExpressionAst)
|
|
{
|
|
var pathAst = (StringConstantExpressionAst)expressionAst;
|
|
// Handle static member completion: echo [int]::<tab>
|
|
var shareMatch = Regex.Match(pathAst.Value, @"^(\[[\w\d\.]+\]::[\w\d\*]*)$");
|
|
if (shareMatch.Success)
|
|
{
|
|
int fakeReplacementIndex, fakeReplacementLength;
|
|
var input = shareMatch.Groups[1].Value;
|
|
var completionParameters = CommandCompletion.MapStringInputToParsedInput(input, input.Length);
|
|
var completionAnalysis = new CompletionAnalysis(completionParameters.Item1, completionParameters.Item2, completionParameters.Item3, context.Options);
|
|
var ret = completionAnalysis.GetResults(
|
|
context.Helper.CurrentPowerShell,
|
|
out fakeReplacementIndex,
|
|
out fakeReplacementLength);
|
|
|
|
if (ret != null && ret.Count > 0)
|
|
{
|
|
string prefix = string.Concat(TokenKind.LParen.Text(), input.AsSpan(0, fakeReplacementIndex));
|
|
foreach (CompletionResult entry in ret)
|
|
{
|
|
string completionText = prefix + entry.CompletionText;
|
|
if (entry.ResultType.Equals(CompletionResultType.Property))
|
|
completionText += TokenKind.RParen.Text();
|
|
result.Add(new CompletionResult(completionText, entry.ListItemText, entry.ResultType,
|
|
entry.ToolTip));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Handle member completion with wildcard: echo $a.*<tab>
|
|
if (pathAst.Value.Contains('*') && secondToLastMemberAst != null &&
|
|
secondToLastMemberAst.Extent.EndLineNumber == pathAst.Extent.StartLineNumber &&
|
|
secondToLastMemberAst.Extent.EndColumnNumber == pathAst.Extent.StartColumnNumber)
|
|
{
|
|
var memberName = pathAst.Value.EndsWith('*')
|
|
? pathAst.Value
|
|
: pathAst.Value + "*";
|
|
var targetExpr = secondToLastMemberAst.Expression;
|
|
if (IsSplattedVariable(targetExpr))
|
|
{
|
|
// It's splatted variable, and the member completion is not useful
|
|
return result;
|
|
}
|
|
|
|
var memberAst = secondToLastMemberAst.Member as StringConstantExpressionAst;
|
|
if (memberAst != null)
|
|
{
|
|
memberName = memberAst.Value + memberName;
|
|
}
|
|
|
|
CompleteMemberHelper(false, memberName, targetExpr, context, result);
|
|
if (result.Count > 0)
|
|
{
|
|
context.ReplacementIndex =
|
|
((InternalScriptPosition)secondToLastMemberAst.Expression.Extent.EndScriptPosition).Offset + 1;
|
|
if (memberAst != null)
|
|
context.ReplacementLength += memberAst.Value.Length;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Treat it as the file name completion
|
|
// Handle this scenario: & 'c:\a b'\<tab>
|
|
string fileName = pathAst.Value;
|
|
if (commandAst.InvocationOperator != TokenKind.Unknown && fileName.IndexOfAny(Utils.Separators.Directory) == 0 &&
|
|
commandAst.CommandElements.Count == 2 && commandAst.CommandElements[0] is StringConstantExpressionAst &&
|
|
commandAst.CommandElements[0].Extent.EndLineNumber == expressionAst.Extent.StartLineNumber &&
|
|
commandAst.CommandElements[0].Extent.EndColumnNumber == expressionAst.Extent.StartColumnNumber)
|
|
{
|
|
if (pseudoBinding != null)
|
|
{
|
|
// CommandElements[0] is resolved to a command
|
|
return result;
|
|
}
|
|
else
|
|
{
|
|
var constantAst = (StringConstantExpressionAst)commandAst.CommandElements[0];
|
|
fileName = constantAst.Value + fileName;
|
|
context.ReplacementIndex = ((InternalScriptPosition)constantAst.Extent.StartScriptPosition).Offset;
|
|
context.ReplacementLength += ((InternalScriptPosition)constantAst.Extent.EndScriptPosition).Offset - context.ReplacementIndex;
|
|
context.WordToComplete = fileName;
|
|
// commandAst.InvocationOperator != TokenKind.Unknown, so we should use literal path
|
|
var clearLiteralPathKey = TurnOnLiteralPathOption(context);
|
|
|
|
try
|
|
{
|
|
return new List<CompletionResult>(CompleteFilename(context));
|
|
}
|
|
finally
|
|
{
|
|
if (clearLiteralPathKey)
|
|
context.Options.Remove("LiteralPaths");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The default argument completion: file path completion, command name completion('WordToComplete' is not empty and contains a dash).
|
|
// If the current argument completion has been process already, we don't go through the default argument completion anymore.
|
|
if (!hasBeenProcessed)
|
|
{
|
|
var commandName = commandAst.GetCommandName();
|
|
var customCompleter = GetCustomArgumentCompleter(
|
|
"NativeArgumentCompleters",
|
|
new[] { commandName, Path.GetFileName(commandName), Path.GetFileNameWithoutExtension(commandName) },
|
|
context);
|
|
if (customCompleter != null)
|
|
{
|
|
if (InvokeScriptArgumentCompleter(
|
|
customCompleter,
|
|
new object[] { context.WordToComplete, commandAst, context.CursorPosition.Offset },
|
|
result))
|
|
{
|
|
return result;
|
|
}
|
|
}
|
|
|
|
var clearLiteralPathKey = false;
|
|
if (pseudoBinding == null)
|
|
{
|
|
// the command could be a native command such as notepad.exe, we use literal path in this case
|
|
clearLiteralPathKey = TurnOnLiteralPathOption(context);
|
|
}
|
|
|
|
try
|
|
{
|
|
result = new List<CompletionResult>(CompleteFilename(context));
|
|
}
|
|
finally
|
|
{
|
|
if (clearLiteralPathKey)
|
|
context.Options.Remove("LiteralPaths");
|
|
}
|
|
|
|
if (context.WordToComplete != string.Empty && context.WordToComplete.Contains('-'))
|
|
{
|
|
var commandResults = CompleteCommand(context);
|
|
if (commandResults != null)
|
|
result.AddRange(commandResults);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
internal static string ConcatenateStringPathArguments(CommandElementAst stringAst, string partialPath, CompletionContext completionContext)
|
|
{
|
|
var constantPathAst = stringAst as StringConstantExpressionAst;
|
|
if (constantPathAst != null)
|
|
{
|
|
string quote = string.Empty;
|
|
switch (constantPathAst.StringConstantType)
|
|
{
|
|
case StringConstantType.SingleQuoted:
|
|
quote = "'";
|
|
break;
|
|
case StringConstantType.DoubleQuoted:
|
|
quote = "\"";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return quote + constantPathAst.Value + partialPath + quote;
|
|
}
|
|
else
|
|
{
|
|
var expandablePathAst = stringAst as ExpandableStringExpressionAst;
|
|
string fullPath = null;
|
|
if (expandablePathAst != null &&
|
|
IsPathSafelyExpandable(expandableStringAst: expandablePathAst,
|
|
extraText: partialPath,
|
|
executionContext: completionContext.ExecutionContext,
|
|
expandedString: out fullPath))
|
|
{
|
|
return fullPath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the argument completion results when the pseudo binding was not successful.
|
|
/// </summary>
|
|
private static List<CompletionResult> GetArgumentCompletionResultsWithFailedPseudoBinding(
|
|
CompletionContext context,
|
|
ArgumentLocation argLocation,
|
|
CommandAst commandAst)
|
|
{
|
|
List<CompletionResult> result = new List<CompletionResult>();
|
|
|
|
PseudoBindingInfo bindingInfo = context.PseudoBindingInfo;
|
|
if (argLocation.IsPositional)
|
|
{
|
|
CompletePositionalArgument(
|
|
bindingInfo.CommandName,
|
|
commandAst,
|
|
context,
|
|
result,
|
|
bindingInfo.UnboundParameters,
|
|
bindingInfo.DefaultParameterSetFlag,
|
|
uint.MaxValue,
|
|
argLocation.Position);
|
|
}
|
|
else
|
|
{
|
|
string paramName = argLocation.Argument.ParameterName;
|
|
WildcardPattern pattern = WildcardPattern.Get(paramName + "*", WildcardOptions.IgnoreCase);
|
|
foreach (MergedCompiledCommandParameter param in bindingInfo.UnboundParameters)
|
|
{
|
|
if (pattern.IsMatch(param.Parameter.Name))
|
|
{
|
|
ProcessParameter(bindingInfo.CommandName, commandAst, context, result, param);
|
|
break;
|
|
}
|
|
|
|
bool isAliasMatch = false;
|
|
foreach (string alias in param.Parameter.Aliases)
|
|
{
|
|
if (pattern.IsMatch(alias))
|
|
{
|
|
isAliasMatch = true;
|
|
ProcessParameter(bindingInfo.CommandName, commandAst, context, result, param);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isAliasMatch)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the argument completion results when the pseudo binding was successful.
|
|
/// </summary>
|
|
private static List<CompletionResult> GetArgumentCompletionResultsWithSuccessfulPseudoBinding(
|
|
CompletionContext context,
|
|
ArgumentLocation argLocation,
|
|
CommandAst commandAst)
|
|
{
|
|
PseudoBindingInfo bindingInfo = context.PseudoBindingInfo;
|
|
Diagnostics.Assert(bindingInfo.InfoType.Equals(PseudoBindingInfoType.PseudoBindingSucceed), "Caller needs to make sure the pseudo binding was successful");
|
|
List<CompletionResult> result = new List<CompletionResult>();
|
|
|
|
if (argLocation.IsPositional && argLocation.Argument == null)
|
|
{
|
|
AstPair lastPositionalArg;
|
|
AstParameterArgumentPair targetPositionalArg =
|
|
FindTargetPositionalArgument(
|
|
bindingInfo.AllParsedArguments,
|
|
argLocation.Position,
|
|
out lastPositionalArg);
|
|
|
|
if (targetPositionalArg != null)
|
|
argLocation.Argument = targetPositionalArg;
|
|
else
|
|
{
|
|
if (lastPositionalArg != null)
|
|
{
|
|
bool lastPositionalGetBound = false;
|
|
Collection<string> parameterNames = new Collection<string>();
|
|
|
|
foreach (KeyValuePair<string, AstParameterArgumentPair> entry in bindingInfo.BoundArguments)
|
|
{
|
|
// positional argument
|
|
if (!entry.Value.ParameterSpecified)
|
|
{
|
|
var arg = (AstPair)entry.Value;
|
|
if (arg.Argument.GetHashCode() == lastPositionalArg.Argument.GetHashCode())
|
|
{
|
|
lastPositionalGetBound = true;
|
|
break;
|
|
}
|
|
}
|
|
else if (entry.Value.ParameterArgumentType.Equals(AstParameterArgumentType.AstArray))
|
|
{
|
|
// check if the positional argument would be bound to a "ValueFromRemainingArgument" parameter
|
|
var arg = (AstArrayPair)entry.Value;
|
|
if (arg.Argument.Any(exp => exp.GetHashCode() == lastPositionalArg.Argument.GetHashCode()))
|
|
{
|
|
parameterNames.Add(entry.Key);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (parameterNames.Count > 0)
|
|
{
|
|
// parameter should be in BoundParameters
|
|
foreach (string param in parameterNames)
|
|
{
|
|
MergedCompiledCommandParameter parameter = bindingInfo.BoundParameters[param];
|
|
ProcessParameter(bindingInfo.CommandName, commandAst, context, result, parameter, bindingInfo.BoundArguments);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
else if (!lastPositionalGetBound)
|
|
{
|
|
// last positional argument was not bound, then positional argument 'tab' wants to
|
|
// expand will not get bound either
|
|
return result;
|
|
}
|
|
}
|
|
|
|
CompletePositionalArgument(
|
|
bindingInfo.CommandName,
|
|
commandAst,
|
|
context,
|
|
result,
|
|
bindingInfo.UnboundParameters,
|
|
bindingInfo.DefaultParameterSetFlag,
|
|
bindingInfo.ValidParameterSetsFlags,
|
|
argLocation.Position,
|
|
bindingInfo.BoundArguments);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
if (argLocation.Argument != null)
|
|
{
|
|
Collection<string> parameterNames = new Collection<string>();
|
|
foreach (KeyValuePair<string, AstParameterArgumentPair> entry in bindingInfo.BoundArguments)
|
|
{
|
|
if (entry.Value.ParameterArgumentType.Equals(AstParameterArgumentType.PipeObject))
|
|
continue;
|
|
|
|
if (entry.Value.ParameterArgumentType.Equals(AstParameterArgumentType.AstArray) && !argLocation.Argument.ParameterSpecified)
|
|
{
|
|
var arrayArg = (AstArrayPair)entry.Value;
|
|
var target = (AstPair)argLocation.Argument;
|
|
if (arrayArg.Argument.Any(exp => exp.GetHashCode() == target.Argument.GetHashCode()))
|
|
{
|
|
parameterNames.Add(entry.Key);
|
|
}
|
|
}
|
|
else if (entry.Value.GetHashCode() == argLocation.Argument.GetHashCode())
|
|
{
|
|
parameterNames.Add(entry.Key);
|
|
}
|
|
}
|
|
|
|
if (parameterNames.Count > 0)
|
|
{
|
|
// those parameters should be in BoundParameters
|
|
foreach (string param in parameterNames)
|
|
{
|
|
MergedCompiledCommandParameter parameter = bindingInfo.BoundParameters[param];
|
|
ProcessParameter(bindingInfo.CommandName, commandAst, context, result, parameter, bindingInfo.BoundArguments);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the positional argument completion results based on the position it's in the command line.
|
|
/// </summary>
|
|
private static void CompletePositionalArgument(
|
|
string commandName,
|
|
CommandAst commandAst,
|
|
CompletionContext context,
|
|
List<CompletionResult> result,
|
|
IEnumerable<MergedCompiledCommandParameter> parameters,
|
|
uint defaultParameterSetFlag,
|
|
uint validParameterSetFlags,
|
|
int position,
|
|
Dictionary<string, AstParameterArgumentPair> boundArguments = null)
|
|
{
|
|
bool isProcessedAsPositional = false;
|
|
bool isDefaultParameterSetValid = defaultParameterSetFlag != 0 &&
|
|
(defaultParameterSetFlag & validParameterSetFlags) != 0;
|
|
MergedCompiledCommandParameter positionalParam = null;
|
|
|
|
foreach (MergedCompiledCommandParameter param in parameters)
|
|
{
|
|
bool isInParameterSet = (param.Parameter.ParameterSetFlags & validParameterSetFlags) != 0 || param.Parameter.IsInAllSets;
|
|
if (!isInParameterSet)
|
|
continue;
|
|
|
|
var parameterSetDataCollection = param.Parameter.GetMatchingParameterSetData(validParameterSetFlags);
|
|
foreach (ParameterSetSpecificMetadata parameterSetData in parameterSetDataCollection)
|
|
{
|
|
// in the first pass, we skip the remaining argument ones
|
|
if (parameterSetData.ValueFromRemainingArguments)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Check the position
|
|
int positionInParameterSet = parameterSetData.Position;
|
|
|
|
if (positionInParameterSet == int.MinValue || positionInParameterSet != position)
|
|
{
|
|
// The parameter is not positional, or its position is not what we want
|
|
continue;
|
|
}
|
|
|
|
if (isDefaultParameterSetValid)
|
|
{
|
|
if (parameterSetData.ParameterSetFlag == defaultParameterSetFlag)
|
|
{
|
|
ProcessParameter(commandName, commandAst, context, result, param, boundArguments);
|
|
isProcessedAsPositional = result.Count > 0;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
if (positionalParam == null)
|
|
positionalParam = param;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
isProcessedAsPositional = true;
|
|
ProcessParameter(commandName, commandAst, context, result, param, boundArguments);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isProcessedAsPositional)
|
|
break;
|
|
}
|
|
|
|
if (!isProcessedAsPositional && positionalParam != null)
|
|
{
|
|
isProcessedAsPositional = true;
|
|
ProcessParameter(commandName, commandAst, context, result, positionalParam, boundArguments);
|
|
}
|
|
|
|
if (!isProcessedAsPositional)
|
|
{
|
|
foreach (MergedCompiledCommandParameter param in parameters)
|
|
{
|
|
bool isInParameterSet = (param.Parameter.ParameterSetFlags & validParameterSetFlags) != 0 || param.Parameter.IsInAllSets;
|
|
if (!isInParameterSet)
|
|
continue;
|
|
|
|
var parameterSetDataCollection = param.Parameter.GetMatchingParameterSetData(validParameterSetFlags);
|
|
foreach (ParameterSetSpecificMetadata parameterSetData in parameterSetDataCollection)
|
|
{
|
|
// in the second pass, we check the remaining argument ones
|
|
if (parameterSetData.ValueFromRemainingArguments)
|
|
{
|
|
ProcessParameter(commandName, commandAst, context, result, param, boundArguments);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process a parameter to get the argument completion results.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the argument completion falls into these pre-defined cases:
|
|
/// 1. The matching parameter is of type Enum
|
|
/// 2. The matching parameter is of type SwitchParameter
|
|
/// 3. The matching parameter is declared with ValidateSetAttribute
|
|
/// 4. Falls into the native command argument completion
|
|
/// a null instance of CompletionResult is added to the end of the
|
|
/// "result" list, to indicate that this particular argument completion
|
|
/// has been processed already. If the "result" list is still empty, we
|
|
/// will not go through the default argument completion steps anymore.
|
|
/// </remarks>
|
|
private static void ProcessParameter(
|
|
string commandName,
|
|
CommandAst commandAst,
|
|
CompletionContext context,
|
|
List<CompletionResult> result,
|
|
MergedCompiledCommandParameter parameter,
|
|
Dictionary<string, AstParameterArgumentPair> boundArguments = null)
|
|
{
|
|
CompletionResult fullMatch = null;
|
|
Type parameterType = GetEffectiveParameterType(parameter.Parameter.Type);
|
|
|
|
if (parameterType.IsArray)
|
|
{
|
|
parameterType = parameterType.GetElementType();
|
|
}
|
|
|
|
if (parameterType.IsEnum)
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
string enumString = LanguagePrimitives.EnumSingleTypeConverter.EnumValues(parameterType);
|
|
string separator = CultureInfo.CurrentUICulture.TextInfo.ListSeparator;
|
|
string[] enumArray = enumString.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
string wordToComplete = context.WordToComplete;
|
|
string quote = HandleDoubleAndSingleQuote(ref wordToComplete);
|
|
|
|
var pattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase);
|
|
var enumList = new List<string>();
|
|
|
|
foreach (string value in enumArray)
|
|
{
|
|
if (wordToComplete.Equals(value, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string completionText = quote == string.Empty ? value : quote + value + quote;
|
|
fullMatch = new CompletionResult(completionText, value, CompletionResultType.ParameterValue, value);
|
|
continue;
|
|
}
|
|
|
|
if (pattern.IsMatch(value))
|
|
{
|
|
enumList.Add(value);
|
|
}
|
|
}
|
|
|
|
if (fullMatch != null)
|
|
{
|
|
result.Add(fullMatch);
|
|
}
|
|
|
|
enumList.Sort();
|
|
result.AddRange(from entry in enumList
|
|
let completionText = quote == string.Empty ? entry : quote + entry + quote
|
|
select new CompletionResult(completionText, entry, CompletionResultType.ParameterValue, entry));
|
|
|
|
result.Add(CompletionResult.Null);
|
|
return;
|
|
}
|
|
|
|
if (parameterType.Equals(typeof(SwitchParameter)))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
if (context.WordToComplete == string.Empty || context.WordToComplete.Equals("$", StringComparison.Ordinal))
|
|
{
|
|
result.Add(new CompletionResult("$true", "$true", CompletionResultType.ParameterValue, "$true"));
|
|
result.Add(new CompletionResult("$false", "$false", CompletionResultType.ParameterValue, "$false"));
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
return;
|
|
}
|
|
|
|
foreach (ValidateArgumentsAttribute att in parameter.Parameter.ValidationAttributes)
|
|
{
|
|
if (att is ValidateSetAttribute setAtt)
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
string wordToComplete = context.WordToComplete;
|
|
string quote = HandleDoubleAndSingleQuote(ref wordToComplete);
|
|
|
|
var pattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase);
|
|
var setList = new List<string>();
|
|
|
|
foreach (string value in setAtt.ValidValues)
|
|
{
|
|
if (value == string.Empty) { continue; }
|
|
|
|
if (wordToComplete.Equals(value, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string completionText = quote == string.Empty ? value : quote + value + quote;
|
|
fullMatch = new CompletionResult(completionText, value, CompletionResultType.ParameterValue, value);
|
|
continue;
|
|
}
|
|
|
|
if (pattern.IsMatch(value))
|
|
{
|
|
setList.Add(value);
|
|
}
|
|
}
|
|
|
|
if (fullMatch != null)
|
|
{
|
|
result.Add(fullMatch);
|
|
}
|
|
|
|
setList.Sort();
|
|
foreach (string entry in setList)
|
|
{
|
|
string realEntry = entry;
|
|
string completionText = entry;
|
|
if (quote == string.Empty)
|
|
{
|
|
if (CompletionRequiresQuotes(entry, false))
|
|
{
|
|
realEntry = CodeGeneration.EscapeSingleQuotedStringContent(entry);
|
|
completionText = "'" + realEntry + "'";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (quote.Equals("'", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
realEntry = CodeGeneration.EscapeSingleQuotedStringContent(entry);
|
|
}
|
|
|
|
completionText = quote + realEntry + quote;
|
|
}
|
|
|
|
result.Add(new CompletionResult(completionText, entry, CompletionResultType.ParameterValue, entry));
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
return;
|
|
}
|
|
}
|
|
|
|
NativeCommandArgumentCompletion(commandName, parameter.Parameter, result, commandAst, context, boundArguments);
|
|
}
|
|
|
|
private static IEnumerable<PSTypeName> NativeCommandArgumentCompletion_InferTypesOfArgument(
|
|
Dictionary<string, AstParameterArgumentPair> boundArguments,
|
|
CommandAst commandAst,
|
|
CompletionContext context,
|
|
string parameterName)
|
|
{
|
|
if (boundArguments == null)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
AstParameterArgumentPair astParameterArgumentPair;
|
|
if (!boundArguments.TryGetValue(parameterName, out astParameterArgumentPair))
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
Ast argumentAst = null;
|
|
switch (astParameterArgumentPair.ParameterArgumentType)
|
|
{
|
|
case AstParameterArgumentType.AstPair:
|
|
{
|
|
AstPair astPair = (AstPair)astParameterArgumentPair;
|
|
argumentAst = astPair.Argument;
|
|
}
|
|
|
|
break;
|
|
|
|
case AstParameterArgumentType.PipeObject:
|
|
{
|
|
var pipelineAst = commandAst.Parent as PipelineAst;
|
|
if (pipelineAst != null)
|
|
{
|
|
int i;
|
|
for (i = 0; i < pipelineAst.PipelineElements.Count; i++)
|
|
{
|
|
if (pipelineAst.PipelineElements[i] == commandAst)
|
|
break;
|
|
}
|
|
|
|
if (i != 0)
|
|
{
|
|
argumentAst = pipelineAst.PipelineElements[i - 1];
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (argumentAst == null)
|
|
{
|
|
yield break;
|
|
}
|
|
|
|
ExpressionAst argumentExpressionAst = argumentAst as ExpressionAst;
|
|
if (argumentExpressionAst == null)
|
|
{
|
|
CommandExpressionAst argumentCommandExpressionAst = argumentAst as CommandExpressionAst;
|
|
if (argumentCommandExpressionAst != null)
|
|
{
|
|
argumentExpressionAst = argumentCommandExpressionAst.Expression;
|
|
}
|
|
}
|
|
|
|
object argumentValue;
|
|
if (argumentExpressionAst != null && SafeExprEvaluator.TrySafeEval(argumentExpressionAst, context.ExecutionContext, out argumentValue))
|
|
{
|
|
if (argumentValue != null)
|
|
{
|
|
IEnumerable enumerable = LanguagePrimitives.GetEnumerable(argumentValue) ??
|
|
new object[] { argumentValue };
|
|
foreach (var element in enumerable)
|
|
{
|
|
if (element == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
PSObject pso = PSObject.AsPSObject(element);
|
|
if ((pso.TypeNames.Count > 0) && (!(pso.TypeNames[0].Equals(pso.BaseObject.GetType().FullName, StringComparison.OrdinalIgnoreCase))))
|
|
{
|
|
yield return new PSTypeName(pso.TypeNames[0]);
|
|
}
|
|
|
|
if (pso.BaseObject is not PSCustomObject)
|
|
{
|
|
yield return new PSTypeName(pso.BaseObject.GetType());
|
|
}
|
|
}
|
|
|
|
yield break;
|
|
}
|
|
}
|
|
|
|
foreach (PSTypeName typeName in AstTypeInference.InferTypeOf(argumentAst, context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval))
|
|
{
|
|
yield return typeName;
|
|
}
|
|
}
|
|
|
|
internal static IList<string> NativeCommandArgumentCompletion_ExtractSecondaryArgument(
|
|
Dictionary<string, AstParameterArgumentPair> boundArguments,
|
|
string parameterName)
|
|
{
|
|
List<string> result = new List<string>();
|
|
|
|
if (boundArguments == null)
|
|
{
|
|
return result;
|
|
}
|
|
|
|
AstParameterArgumentPair argumentValue;
|
|
if (!boundArguments.TryGetValue(parameterName, out argumentValue))
|
|
{
|
|
return result;
|
|
}
|
|
|
|
switch (argumentValue.ParameterArgumentType)
|
|
{
|
|
case AstParameterArgumentType.AstPair:
|
|
{
|
|
var value = (AstPair)argumentValue;
|
|
if (value.Argument is StringConstantExpressionAst)
|
|
{
|
|
var argument = (StringConstantExpressionAst)value.Argument;
|
|
result.Add(argument.Value);
|
|
}
|
|
else if (value.Argument is ArrayLiteralAst)
|
|
{
|
|
var argument = (ArrayLiteralAst)value.Argument;
|
|
foreach (ExpressionAst entry in argument.Elements)
|
|
{
|
|
var entryAsString = entry as StringConstantExpressionAst;
|
|
if (entryAsString != null)
|
|
{
|
|
result.Add(entryAsString.Value);
|
|
}
|
|
else
|
|
{
|
|
result.Clear();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case AstParameterArgumentType.AstArray:
|
|
{
|
|
var value = (AstArrayPair)argumentValue;
|
|
var argument = value.Argument;
|
|
|
|
foreach (ExpressionAst entry in argument)
|
|
{
|
|
var entryAsString = entry as StringConstantExpressionAst;
|
|
if (entryAsString != null)
|
|
{
|
|
result.Add(entryAsString.Value);
|
|
}
|
|
else
|
|
{
|
|
result.Clear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static void NativeCommandArgumentCompletion(
|
|
string commandName,
|
|
CompiledCommandParameter parameter,
|
|
List<CompletionResult> result,
|
|
CommandAst commandAst,
|
|
CompletionContext context,
|
|
Dictionary<string, AstParameterArgumentPair> boundArguments = null)
|
|
{
|
|
string parameterName = parameter.Name;
|
|
|
|
// Fall back to the commandAst command name if a command name is not found. This can be caused by a script block or AST with the matching function definition being passed to CompleteInput
|
|
// This allows for editors and other tools using CompleteInput with Script/AST definations to get values from RegisteredArgumentCompleters to better match the console experience.
|
|
// See issue https://github.com/PowerShell/PowerShell/issues/10567
|
|
string actualCommandName = string.IsNullOrEmpty(commandName)
|
|
? commandAst.GetCommandName()
|
|
: commandName;
|
|
|
|
if (string.IsNullOrEmpty(actualCommandName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string parameterFullName = $"{actualCommandName}:{parameterName}";
|
|
|
|
ScriptBlock customCompleter = GetCustomArgumentCompleter(
|
|
"CustomArgumentCompleters",
|
|
new[] { parameterFullName, parameterName },
|
|
context);
|
|
|
|
if (customCompleter != null)
|
|
{
|
|
if (InvokeScriptArgumentCompleter(
|
|
customCompleter,
|
|
commandName, parameterName, context.WordToComplete, commandAst, context,
|
|
result))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
var argumentCompleterAttribute = parameter.CompiledAttributes.OfType<ArgumentCompleterAttribute>().FirstOrDefault();
|
|
if (argumentCompleterAttribute != null)
|
|
{
|
|
try
|
|
{
|
|
var completer = argumentCompleterAttribute.CreateArgumentCompleter();
|
|
|
|
if (completer != null)
|
|
{
|
|
var customResults = completer.CompleteArgument(commandName, parameterName,
|
|
context.WordToComplete, commandAst, GetBoundArgumentsAsHashtable(context));
|
|
if (customResults != null)
|
|
{
|
|
result.AddRange(customResults);
|
|
result.Add(CompletionResult.Null);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (InvokeScriptArgumentCompleter(
|
|
argumentCompleterAttribute.ScriptBlock,
|
|
commandName, parameterName, context.WordToComplete, commandAst, context,
|
|
result))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
var argumentCompletionsAttribute = parameter.CompiledAttributes.OfType<ArgumentCompletionsAttribute>().FirstOrDefault();
|
|
if (argumentCompletionsAttribute != null)
|
|
{
|
|
var customResults = argumentCompletionsAttribute.CompleteArgument(commandName, parameterName,
|
|
context.WordToComplete, commandAst, GetBoundArgumentsAsHashtable(context));
|
|
if (customResults != null)
|
|
{
|
|
result.AddRange(customResults);
|
|
result.Add(CompletionResult.Null);
|
|
return;
|
|
}
|
|
}
|
|
|
|
switch (commandName)
|
|
{
|
|
case "Get-Command":
|
|
{
|
|
if (parameterName.Equals("Module", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionGetCommand(context, /* moduleName: */ null, parameterName, result);
|
|
break;
|
|
}
|
|
|
|
if (parameterName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var moduleNames = NativeCommandArgumentCompletion_ExtractSecondaryArgument(boundArguments, "Module");
|
|
|
|
if (moduleNames.Count > 0)
|
|
{
|
|
foreach (string module in moduleNames)
|
|
{
|
|
NativeCompletionGetCommand(context, module, parameterName, result);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NativeCompletionGetCommand(context, /* moduleName: */ null, parameterName, result);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
if (parameterName.Equals("ParameterType", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionTypeName(context, result);
|
|
break;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "Show-Command":
|
|
{
|
|
NativeCompletionGetHelpCommand(context, parameterName, /* isHelpRelated: */ false, result);
|
|
break;
|
|
}
|
|
case "help":
|
|
case "Get-Help":
|
|
{
|
|
NativeCompletionGetHelpCommand(context, parameterName, /* isHelpRelated: */ true, result);
|
|
break;
|
|
}
|
|
case "Invoke-Expression":
|
|
{
|
|
if (parameterName.Equals("Command", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var commandResults = CompleteCommand(context);
|
|
if (commandResults != null)
|
|
result.AddRange(commandResults);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "Clear-EventLog":
|
|
case "Get-EventLog":
|
|
case "Limit-EventLog":
|
|
case "Remove-EventLog":
|
|
case "Write-EventLog":
|
|
{
|
|
NativeCompletionEventLogCommands(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "Get-Job":
|
|
case "Receive-Job":
|
|
case "Remove-Job":
|
|
case "Stop-Job":
|
|
case "Wait-Job":
|
|
case "Suspend-Job":
|
|
case "Resume-Job":
|
|
{
|
|
NativeCompletionJobCommands(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "Disable-ScheduledJob":
|
|
case "Enable-ScheduledJob":
|
|
case "Get-ScheduledJob":
|
|
case "Unregister-ScheduledJob":
|
|
{
|
|
NativeCompletionScheduledJobCommands(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "Get-Module":
|
|
{
|
|
bool loadedModulesOnly = boundArguments == null || !boundArguments.ContainsKey("ListAvailable");
|
|
bool skipEditionCheck = !loadedModulesOnly && boundArguments.ContainsKey("SkipEditionCheck");
|
|
NativeCompletionModuleCommands(context, parameterName, result, loadedModulesOnly, skipEditionCheck: skipEditionCheck);
|
|
break;
|
|
}
|
|
case "Remove-Module":
|
|
{
|
|
NativeCompletionModuleCommands(context, parameterName, result, loadedModulesOnly: true);
|
|
break;
|
|
}
|
|
case "Import-Module":
|
|
{
|
|
bool skipEditionCheck = boundArguments != null && boundArguments.ContainsKey("SkipEditionCheck");
|
|
NativeCompletionModuleCommands(context, parameterName, result, isImportModule: true, skipEditionCheck: skipEditionCheck);
|
|
break;
|
|
}
|
|
case "Debug-Process":
|
|
case "Get-Process":
|
|
case "Stop-Process":
|
|
case "Wait-Process":
|
|
case "Enter-PSHostProcess":
|
|
{
|
|
NativeCompletionProcessCommands(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "Get-PSDrive":
|
|
case "Remove-PSDrive":
|
|
{
|
|
if (parameterName.Equals("PSProvider", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionProviderCommands(context, parameterName, result);
|
|
}
|
|
else if (parameterName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var psProviders = NativeCommandArgumentCompletion_ExtractSecondaryArgument(boundArguments, "PSProvider");
|
|
if (psProviders.Count > 0)
|
|
{
|
|
foreach (string psProvider in psProviders)
|
|
{
|
|
NativeCompletionDriveCommands(context, psProvider, parameterName, result);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NativeCompletionDriveCommands(context, /* psProvider: */ null, parameterName, result);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "New-PSDrive":
|
|
{
|
|
NativeCompletionProviderCommands(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "Get-PSProvider":
|
|
{
|
|
NativeCompletionProviderCommands(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "Get-Service":
|
|
case "Start-Service":
|
|
case "Restart-Service":
|
|
case "Resume-Service":
|
|
case "Set-Service":
|
|
case "Stop-Service":
|
|
case "Suspend-Service":
|
|
{
|
|
NativeCompletionServiceCommands(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "Clear-Variable":
|
|
case "Get-Variable":
|
|
case "Remove-Variable":
|
|
case "Set-Variable":
|
|
{
|
|
NativeCompletionVariableCommands(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "Get-Alias":
|
|
{
|
|
NativeCompletionAliasCommands(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "Get-TraceSource":
|
|
case "Set-TraceSource":
|
|
case "Trace-Command":
|
|
{
|
|
NativeCompletionTraceSourceCommands(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "Push-Location":
|
|
case "Set-Location":
|
|
{
|
|
NativeCompletionSetLocationCommand(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "Move-Item":
|
|
case "Copy-Item":
|
|
{
|
|
NativeCompletionCopyMoveItemCommand(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "New-Item":
|
|
{
|
|
NativeCompletionNewItemCommand(context, parameterName, result);
|
|
break;
|
|
}
|
|
case "ForEach-Object":
|
|
{
|
|
if (parameterName.Equals("MemberName", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionMemberName(context, result, commandAst);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "Group-Object":
|
|
case "Measure-Object":
|
|
case "Sort-Object":
|
|
case "Where-Object":
|
|
{
|
|
if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionMemberName(context, result, commandAst);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "Format-Custom":
|
|
case "Format-List":
|
|
case "Format-Table":
|
|
case "Format-Wide":
|
|
{
|
|
if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionMemberName(context, result, commandAst);
|
|
}
|
|
else if (parameterName.Equals("View", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionFormatViewName(context, boundArguments, result, commandAst, commandName);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case "Select-Object":
|
|
{
|
|
if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase)
|
|
|| parameterName.Equals("ExcludeProperty", StringComparison.OrdinalIgnoreCase)
|
|
|| parameterName.Equals("ExpandProperty", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionMemberName(context, result, commandAst);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case "New-Object":
|
|
{
|
|
if (parameterName.Equals("TypeName", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionTypeName(context, result);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case "Get-CimClass":
|
|
case "Get-CimInstance":
|
|
case "Get-CimAssociatedInstance":
|
|
case "Invoke-CimMethod":
|
|
case "New-CimInstance":
|
|
case "Register-CimIndicationEvent":
|
|
{
|
|
NativeCompletionCimCommands(parameterName, boundArguments, result, commandAst, context);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
NativeCompletionPathArgument(context, parameterName, result);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Hashtable GetBoundArgumentsAsHashtable(CompletionContext context)
|
|
{
|
|
var result = new Hashtable(StringComparer.OrdinalIgnoreCase);
|
|
if (context.PseudoBindingInfo != null)
|
|
{
|
|
var boundArguments = context.PseudoBindingInfo.BoundArguments;
|
|
if (boundArguments != null)
|
|
{
|
|
foreach (var boundArgument in boundArguments)
|
|
{
|
|
var astPair = boundArgument.Value as AstPair;
|
|
if (astPair != null)
|
|
{
|
|
var parameterAst = astPair.Argument as CommandParameterAst;
|
|
var exprAst = parameterAst != null
|
|
? parameterAst.Argument
|
|
: astPair.Argument as ExpressionAst;
|
|
object value;
|
|
if (exprAst != null && SafeExprEvaluator.TrySafeEval(exprAst, context.ExecutionContext, out value))
|
|
{
|
|
result[boundArgument.Key] = value;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
var switchPair = boundArgument.Value as SwitchPair;
|
|
if (switchPair != null)
|
|
{
|
|
result[boundArgument.Key] = switchPair.Argument;
|
|
continue;
|
|
}
|
|
// Ignored:
|
|
// AstArrayPair - only used for ValueFromRemainingArguments, not that useful for tab completion
|
|
// FakePair - missing argument, not that useful
|
|
// PipeObjectPair - no actual argument, makes for a poor api
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static ScriptBlock GetCustomArgumentCompleter(
|
|
string optionKey,
|
|
IEnumerable<string> keys,
|
|
CompletionContext context)
|
|
{
|
|
ScriptBlock scriptBlock;
|
|
var options = context.Options;
|
|
if (options != null)
|
|
{
|
|
var customCompleters = options[optionKey] as Hashtable;
|
|
if (customCompleters != null)
|
|
{
|
|
foreach (var key in keys)
|
|
{
|
|
if (customCompleters.ContainsKey(key))
|
|
{
|
|
scriptBlock = customCompleters[key] as ScriptBlock;
|
|
if (scriptBlock != null)
|
|
return scriptBlock;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var registeredCompleters = optionKey.Equals("NativeArgumentCompleters", StringComparison.OrdinalIgnoreCase)
|
|
? context.NativeArgumentCompleters
|
|
: context.CustomArgumentCompleters;
|
|
|
|
if (registeredCompleters != null)
|
|
{
|
|
foreach (var key in keys)
|
|
{
|
|
if (registeredCompleters.TryGetValue(key, out scriptBlock))
|
|
{
|
|
return scriptBlock;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool InvokeScriptArgumentCompleter(
|
|
ScriptBlock scriptBlock,
|
|
string commandName,
|
|
string parameterName,
|
|
string wordToComplete,
|
|
CommandAst commandAst,
|
|
CompletionContext context,
|
|
List<CompletionResult> resultList)
|
|
{
|
|
bool result = InvokeScriptArgumentCompleter(
|
|
scriptBlock,
|
|
new object[] { commandName, parameterName, wordToComplete, commandAst, GetBoundArgumentsAsHashtable(context) },
|
|
resultList);
|
|
if (result)
|
|
{
|
|
resultList.Add(CompletionResult.Null);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static bool InvokeScriptArgumentCompleter(
|
|
ScriptBlock scriptBlock,
|
|
object[] argumentsToCompleter,
|
|
List<CompletionResult> result)
|
|
{
|
|
Collection<PSObject> customResults = null;
|
|
try
|
|
{
|
|
customResults = scriptBlock.Invoke(argumentsToCompleter);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
|
|
if (customResults == null || customResults.Count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var customResult in customResults)
|
|
{
|
|
var resultAsCompletion = customResult.BaseObject as CompletionResult;
|
|
if (resultAsCompletion != null)
|
|
{
|
|
result.Add(resultAsCompletion);
|
|
continue;
|
|
}
|
|
|
|
var resultAsString = customResult.ToString();
|
|
result.Add(new CompletionResult(resultAsString));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// All the methods for native command argument completion will add a null instance of the type CompletionResult to the end of the
|
|
// "result" list, to indicate that this particular argument completion has fallen into one of the native command argument completion methods,
|
|
// and has been processed already. So if the "result" list is still empty afterward, we will not go through the default argument completion anymore.
|
|
#region Native Command Argument Completion
|
|
|
|
private static void RemoveLastNullCompletionResult(List<CompletionResult> result)
|
|
{
|
|
if (result.Count > 0 && result[result.Count - 1].Equals(CompletionResult.Null))
|
|
{
|
|
result.RemoveAt(result.Count - 1);
|
|
}
|
|
}
|
|
|
|
private static void NativeCompletionCimCommands(
|
|
string parameter,
|
|
Dictionary<string, AstParameterArgumentPair> boundArguments,
|
|
List<CompletionResult> result,
|
|
CommandAst commandAst,
|
|
CompletionContext context)
|
|
{
|
|
if (boundArguments != null)
|
|
{
|
|
AstParameterArgumentPair astParameterArgumentPair;
|
|
if ((boundArguments.TryGetValue("ComputerName", out astParameterArgumentPair)
|
|
|| boundArguments.TryGetValue("CimSession", out astParameterArgumentPair))
|
|
&& astParameterArgumentPair != null)
|
|
{
|
|
switch (astParameterArgumentPair.ParameterArgumentType)
|
|
{
|
|
case AstParameterArgumentType.PipeObject:
|
|
case AstParameterArgumentType.Fake:
|
|
break;
|
|
|
|
default:
|
|
return; // we won't tab-complete remote class names
|
|
}
|
|
}
|
|
}
|
|
|
|
if (parameter.Equals("Namespace", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionCimNamespace(result, context);
|
|
result.Add(CompletionResult.Null);
|
|
return;
|
|
}
|
|
|
|
string pseudoboundCimNamespace = NativeCommandArgumentCompletion_ExtractSecondaryArgument(boundArguments, "Namespace").FirstOrDefault();
|
|
if (parameter.Equals("ClassName", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionCimClassName(pseudoboundCimNamespace, result, context);
|
|
result.Add(CompletionResult.Null);
|
|
return;
|
|
}
|
|
|
|
bool gotInstance = false;
|
|
IEnumerable<PSTypeName> cimClassTypeNames = null;
|
|
string pseudoboundClassName = NativeCommandArgumentCompletion_ExtractSecondaryArgument(boundArguments, "ClassName").FirstOrDefault();
|
|
if (pseudoboundClassName != null)
|
|
{
|
|
gotInstance = false;
|
|
var tmp = new List<PSTypeName>();
|
|
tmp.Add(new PSTypeName(typeof(CimInstance).FullName + "#" + (pseudoboundCimNamespace ?? "root/cimv2") + "/" + pseudoboundClassName));
|
|
cimClassTypeNames = tmp;
|
|
}
|
|
else if (boundArguments != null && boundArguments.ContainsKey("InputObject"))
|
|
{
|
|
gotInstance = true;
|
|
cimClassTypeNames = NativeCommandArgumentCompletion_InferTypesOfArgument(boundArguments, commandAst, context, "InputObject");
|
|
}
|
|
|
|
if (cimClassTypeNames != null)
|
|
{
|
|
foreach (PSTypeName typeName in cimClassTypeNames)
|
|
{
|
|
if (TypeInferenceContext.ParseCimCommandsTypeName(typeName, out pseudoboundCimNamespace, out pseudoboundClassName))
|
|
{
|
|
if (parameter.Equals("ResultClassName", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionCimAssociationResultClassName(pseudoboundCimNamespace, pseudoboundClassName, result, context);
|
|
}
|
|
else if (parameter.Equals("MethodName", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionCimMethodName(pseudoboundCimNamespace, pseudoboundClassName, !gotInstance, result, context);
|
|
}
|
|
}
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
}
|
|
|
|
private static readonly ConcurrentDictionary<string, IEnumerable<string>> s_cimNamespaceAndClassNameToAssociationResultClassNames =
|
|
new ConcurrentDictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private static IEnumerable<string> NativeCompletionCimAssociationResultClassName_GetResultClassNames(
|
|
string cimNamespaceOfSource,
|
|
string cimClassNameOfSource)
|
|
{
|
|
StringBuilder safeClassName = new StringBuilder();
|
|
foreach (char c in cimClassNameOfSource)
|
|
{
|
|
if (char.IsLetterOrDigit(c) || c == '_')
|
|
{
|
|
safeClassName.Append(c);
|
|
}
|
|
}
|
|
|
|
List<string> resultClassNames = new List<string>();
|
|
using (var cimSession = CimSession.Create(null))
|
|
{
|
|
CimClass cimClass = cimSession.GetClass(cimNamespaceOfSource ?? "root/cimv2", cimClassNameOfSource);
|
|
while (cimClass != null)
|
|
{
|
|
string query = string.Format(
|
|
CultureInfo.InvariantCulture,
|
|
"associators of {{{0}}} WHERE SchemaOnly",
|
|
cimClass.CimSystemProperties.ClassName);
|
|
|
|
resultClassNames.AddRange(
|
|
cimSession.QueryInstances(cimNamespaceOfSource ?? "root/cimv2", "WQL", query)
|
|
.Select(static associationInstance => associationInstance.CimSystemProperties.ClassName));
|
|
|
|
cimClass = cimClass.CimSuperClass;
|
|
}
|
|
}
|
|
|
|
resultClassNames.Sort(StringComparer.OrdinalIgnoreCase);
|
|
|
|
return resultClassNames.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
|
}
|
|
|
|
private static void NativeCompletionCimAssociationResultClassName(
|
|
string pseudoboundNamespace,
|
|
string pseudoboundClassName,
|
|
List<CompletionResult> result,
|
|
CompletionContext context)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(pseudoboundClassName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
IEnumerable<string> resultClassNames = s_cimNamespaceAndClassNameToAssociationResultClassNames.GetOrAdd(
|
|
(pseudoboundNamespace ?? "root/cimv2") + ":" + pseudoboundClassName,
|
|
_ => NativeCompletionCimAssociationResultClassName_GetResultClassNames(pseudoboundNamespace, pseudoboundClassName));
|
|
|
|
WildcardPattern resultClassNamePattern = WildcardPattern.Get(context.WordToComplete + "*", WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant);
|
|
result.AddRange(resultClassNames
|
|
.Where(resultClassNamePattern.IsMatch)
|
|
.Select(x => new CompletionResult(x, x, CompletionResultType.Type, string.Format(CultureInfo.InvariantCulture, "{0} -> {1}", pseudoboundClassName, x))));
|
|
}
|
|
|
|
private static void NativeCompletionCimMethodName(
|
|
string pseudoboundNamespace,
|
|
string pseudoboundClassName,
|
|
bool staticMethod,
|
|
List<CompletionResult> result,
|
|
CompletionContext context)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(pseudoboundClassName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
CimClass cimClass;
|
|
using (var cimSession = CimSession.Create(null))
|
|
{
|
|
cimClass = cimSession.GetClass(pseudoboundNamespace ?? "root/cimv2", pseudoboundClassName);
|
|
}
|
|
|
|
WildcardPattern methodNamePattern = WildcardPattern.Get(context.WordToComplete + "*", WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase);
|
|
List<CompletionResult> localResults = new List<CompletionResult>();
|
|
foreach (CimMethodDeclaration methodDeclaration in cimClass.CimClassMethods)
|
|
{
|
|
string methodName = methodDeclaration.Name;
|
|
if (!methodNamePattern.IsMatch(methodName))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool currentMethodIsStatic = methodDeclaration.Qualifiers.Any(static q => q.Name.Equals("Static", StringComparison.OrdinalIgnoreCase));
|
|
if ((currentMethodIsStatic && !staticMethod) || (!currentMethodIsStatic && staticMethod))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
StringBuilder tooltipText = new StringBuilder();
|
|
tooltipText.Append(methodName);
|
|
tooltipText.Append('(');
|
|
bool gotFirstParameter = false;
|
|
foreach (var methodParameter in methodDeclaration.Parameters)
|
|
{
|
|
bool outParameter = methodParameter.Qualifiers.Any(static q => q.Name.Equals("Out", StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (!gotFirstParameter)
|
|
{
|
|
gotFirstParameter = true;
|
|
}
|
|
else
|
|
{
|
|
tooltipText.Append(", ");
|
|
}
|
|
|
|
if (outParameter)
|
|
{
|
|
tooltipText.Append("[out] ");
|
|
}
|
|
|
|
tooltipText.Append(CimInstanceAdapter.CimTypeToTypeNameDisplayString(methodParameter.CimType));
|
|
tooltipText.Append(' ');
|
|
tooltipText.Append(methodParameter.Name);
|
|
|
|
if (outParameter)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
tooltipText.Append(')');
|
|
|
|
localResults.Add(new CompletionResult(methodName, methodName, CompletionResultType.Method, tooltipText.ToString()));
|
|
}
|
|
|
|
result.AddRange(localResults.OrderBy(static x => x.ListItemText, StringComparer.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private static readonly ConcurrentDictionary<string, IEnumerable<string>> s_cimNamespaceToClassNames =
|
|
new ConcurrentDictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
private static IEnumerable<string> NativeCompletionCimClassName_GetClassNames(string targetNamespace)
|
|
{
|
|
List<string> result = new List<string>();
|
|
using (CimSession cimSession = CimSession.Create(null))
|
|
{
|
|
using (var operationOptions = new CimOperationOptions { ClassNamesOnly = true })
|
|
foreach (CimClass cimClass in cimSession.EnumerateClasses(targetNamespace, null, operationOptions))
|
|
using (cimClass)
|
|
{
|
|
string className = cimClass.CimSystemProperties.ClassName;
|
|
result.Add(className);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static void NativeCompletionCimClassName(
|
|
string pseudoBoundNamespace,
|
|
List<CompletionResult> result,
|
|
CompletionContext context)
|
|
{
|
|
string targetNamespace = pseudoBoundNamespace ?? "root/cimv2";
|
|
|
|
List<string> regularClasses = new List<string>();
|
|
List<string> systemClasses = new List<string>();
|
|
|
|
IEnumerable<string> allClasses = s_cimNamespaceToClassNames.GetOrAdd(
|
|
targetNamespace,
|
|
NativeCompletionCimClassName_GetClassNames);
|
|
WildcardPattern classNamePattern = WildcardPattern.Get(context.WordToComplete + "*", WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase);
|
|
|
|
foreach (string className in allClasses)
|
|
{
|
|
if (context.Helper.CancelTabCompletion)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (!classNamePattern.IsMatch(className))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (className.Length > 0 && className[0] == '_')
|
|
{
|
|
systemClasses.Add(className);
|
|
}
|
|
else
|
|
{
|
|
regularClasses.Add(className);
|
|
}
|
|
}
|
|
|
|
regularClasses.Sort(StringComparer.OrdinalIgnoreCase);
|
|
systemClasses.Sort(StringComparer.OrdinalIgnoreCase);
|
|
result.AddRange(
|
|
regularClasses.Concat(systemClasses)
|
|
.Select(className => new CompletionResult(className, className, CompletionResultType.Type, targetNamespace + ":" + className)));
|
|
}
|
|
|
|
private static void NativeCompletionCimNamespace(
|
|
List<CompletionResult> result,
|
|
CompletionContext context)
|
|
{
|
|
string containerNamespace = "root";
|
|
string prefixOfChildNamespace = string.Empty;
|
|
if (!string.IsNullOrEmpty(context.WordToComplete))
|
|
{
|
|
int lastSlashOrBackslash = context.WordToComplete.LastIndexOfAny(Utils.Separators.Directory);
|
|
if (lastSlashOrBackslash != (-1))
|
|
{
|
|
containerNamespace = context.WordToComplete.Substring(0, lastSlashOrBackslash);
|
|
prefixOfChildNamespace = context.WordToComplete.Substring(lastSlashOrBackslash + 1);
|
|
}
|
|
}
|
|
|
|
List<CompletionResult> namespaceResults = new List<CompletionResult>();
|
|
WildcardPattern childNamespacePattern = WildcardPattern.Get(prefixOfChildNamespace + "*", WildcardOptions.IgnoreCase | WildcardOptions.CultureInvariant);
|
|
using (CimSession cimSession = CimSession.Create(null))
|
|
{
|
|
foreach (CimInstance namespaceInstance in cimSession.EnumerateInstances(containerNamespace, "__Namespace"))
|
|
using (namespaceInstance)
|
|
{
|
|
if (context.Helper.CancelTabCompletion)
|
|
{
|
|
break;
|
|
}
|
|
|
|
CimProperty namespaceNameProperty = namespaceInstance.CimInstanceProperties["Name"];
|
|
if (namespaceNameProperty == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!(namespaceNameProperty.Value is string childNamespace))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!childNamespacePattern.IsMatch(childNamespace))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
namespaceResults.Add(new CompletionResult(
|
|
containerNamespace + "/" + childNamespace,
|
|
childNamespace,
|
|
CompletionResultType.Namespace,
|
|
containerNamespace + "/" + childNamespace));
|
|
}
|
|
}
|
|
|
|
result.AddRange(namespaceResults.OrderBy(static x => x.ListItemText, StringComparer.OrdinalIgnoreCase));
|
|
}
|
|
|
|
private static void NativeCompletionGetCommand(CompletionContext context, string moduleName, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (!string.IsNullOrEmpty(paramName) && paramName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
// Available commands
|
|
var commandResults = CompleteCommand(context, moduleName);
|
|
if (commandResults != null)
|
|
result.AddRange(commandResults);
|
|
|
|
// Consider files only if the -Module parameter is not present
|
|
if (moduleName == null)
|
|
{
|
|
// ps1 files and directories. We only complete the files with .ps1 extension for Get-Command, because the -Syntax
|
|
// may only works on files with .ps1 extension
|
|
var ps1Extension = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { StringLiterals.PowerShellScriptFileExtension };
|
|
var moduleFilesResults = new List<CompletionResult>(CompleteFilename(context, /* containerOnly: */ false, ps1Extension));
|
|
if (moduleFilesResults.Count > 0)
|
|
result.AddRange(moduleFilesResults);
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
else if (!string.IsNullOrEmpty(paramName) && paramName.Equals("Module", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
var modules = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var moduleResults = CompleteModuleName(context, loadedModulesOnly: true);
|
|
if (moduleResults != null)
|
|
{
|
|
foreach (CompletionResult moduleResult in moduleResults)
|
|
{
|
|
if (!modules.Contains(moduleResult.ToolTip))
|
|
{
|
|
modules.Add(moduleResult.ToolTip);
|
|
result.Add(moduleResult);
|
|
}
|
|
}
|
|
}
|
|
|
|
moduleResults = CompleteModuleName(context, loadedModulesOnly: false);
|
|
if (moduleResults != null)
|
|
{
|
|
foreach (CompletionResult moduleResult in moduleResults)
|
|
{
|
|
if (!modules.Contains(moduleResult.ToolTip))
|
|
{
|
|
modules.Add(moduleResult.ToolTip);
|
|
result.Add(moduleResult);
|
|
}
|
|
}
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
}
|
|
|
|
private static void NativeCompletionGetHelpCommand(CompletionContext context, string paramName, bool isHelpRelated, List<CompletionResult> result)
|
|
{
|
|
if (!string.IsNullOrEmpty(paramName) && paramName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
// Available commands
|
|
const CommandTypes commandTypes = CommandTypes.Cmdlet | CommandTypes.Function | CommandTypes.Alias | CommandTypes.ExternalScript | CommandTypes.Configuration;
|
|
var commandResults = CompleteCommand(context, /* moduleName: */ null, commandTypes);
|
|
if (commandResults != null)
|
|
result.AddRange(commandResults);
|
|
|
|
// ps1 files and directories
|
|
var ps1Extension = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { StringLiterals.PowerShellScriptFileExtension };
|
|
var fileResults = new List<CompletionResult>(CompleteFilename(context, /* containerOnly: */ false, ps1Extension));
|
|
if (fileResults.Count > 0)
|
|
result.AddRange(fileResults);
|
|
|
|
if (isHelpRelated)
|
|
{
|
|
// Available topics
|
|
var helpTopicResults = CompleteHelpTopics(context);
|
|
if (helpTopicResults != null)
|
|
result.AddRange(helpTopicResults);
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
}
|
|
|
|
private static void NativeCompletionEventLogCommands(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (!string.IsNullOrEmpty(paramName) && paramName.Equals("LogName", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
var logName = context.WordToComplete ?? string.Empty;
|
|
var quote = HandleDoubleAndSingleQuote(ref logName);
|
|
|
|
if (!logName.EndsWith('*'))
|
|
{
|
|
logName += "*";
|
|
}
|
|
|
|
var pattern = WildcardPattern.Get(logName, WildcardOptions.IgnoreCase);
|
|
|
|
var powerShellExecutionHelper = context.Helper;
|
|
var powershell = powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-EventLog").AddParameter("LogName", "*");
|
|
|
|
Exception exceptionThrown;
|
|
var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
|
|
if (psObjects != null)
|
|
{
|
|
foreach (dynamic eventLog in psObjects)
|
|
{
|
|
var completionText = eventLog.Log.ToString();
|
|
var listItemText = completionText;
|
|
|
|
if (CompletionRequiresQuotes(completionText, false))
|
|
{
|
|
var quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
completionText = quote + completionText + quote;
|
|
}
|
|
|
|
if (pattern.IsMatch(listItemText))
|
|
{
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
}
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
}
|
|
|
|
private static void NativeCompletionJobCommands(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName))
|
|
return;
|
|
|
|
var wordToComplete = context.WordToComplete ?? string.Empty;
|
|
var quote = HandleDoubleAndSingleQuote(ref wordToComplete);
|
|
|
|
if (!wordToComplete.EndsWith('*'))
|
|
{
|
|
wordToComplete += "*";
|
|
}
|
|
|
|
var pattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase);
|
|
|
|
var paramIsName = paramName.Equals("Name", StringComparison.OrdinalIgnoreCase);
|
|
var (parameterName, value) = paramIsName ? ("Name", wordToComplete) : ("IncludeChildJob", (object)true);
|
|
var powerShellExecutionHelper = context.Helper;
|
|
powerShellExecutionHelper.AddCommandWithPreferenceSetting("Get-Job", typeof(GetJobCommand)).AddParameter(parameterName, value);
|
|
|
|
Exception exceptionThrown;
|
|
var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
if (psObjects == null)
|
|
return;
|
|
|
|
if (paramName.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
foreach (dynamic psJob in psObjects)
|
|
{
|
|
var completionText = psJob.Id.ToString();
|
|
if (pattern.IsMatch(completionText))
|
|
{
|
|
var listItemText = completionText;
|
|
completionText = quote + completionText + quote;
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
else if (paramName.Equals("InstanceId", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
foreach (dynamic psJob in psObjects)
|
|
{
|
|
var completionText = psJob.InstanceId.ToString();
|
|
if (pattern.IsMatch(completionText))
|
|
{
|
|
var listItemText = completionText;
|
|
completionText = quote + completionText + quote;
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
else if (paramIsName)
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
foreach (dynamic psJob in psObjects)
|
|
{
|
|
var completionText = psJob.Name;
|
|
var listItemText = completionText;
|
|
|
|
if (CompletionRequiresQuotes(completionText, false))
|
|
{
|
|
var quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
completionText = quote + completionText + quote;
|
|
}
|
|
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
}
|
|
|
|
private static void NativeCompletionScheduledJobCommands(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName))
|
|
return;
|
|
|
|
var wordToComplete = context.WordToComplete ?? string.Empty;
|
|
var quote = HandleDoubleAndSingleQuote(ref wordToComplete);
|
|
|
|
if (!wordToComplete.EndsWith('*'))
|
|
{
|
|
wordToComplete += "*";
|
|
}
|
|
|
|
var pattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase);
|
|
|
|
var powerShellExecutionHelper = context.Helper;
|
|
if (paramName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
powerShellExecutionHelper.AddCommandWithPreferenceSetting("PSScheduledJob\\Get-ScheduledJob").AddParameter("Name", wordToComplete);
|
|
}
|
|
else
|
|
{
|
|
powerShellExecutionHelper.AddCommandWithPreferenceSetting("PSScheduledJob\\Get-ScheduledJob");
|
|
}
|
|
|
|
Exception exceptionThrown;
|
|
var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
if (psObjects == null)
|
|
return;
|
|
|
|
if (paramName.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
foreach (dynamic psJob in psObjects)
|
|
{
|
|
var completionText = psJob.Id.ToString();
|
|
if (pattern.IsMatch(completionText))
|
|
{
|
|
var listItemText = completionText;
|
|
completionText = quote + completionText + quote;
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
else if (paramName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
foreach (dynamic psJob in psObjects)
|
|
{
|
|
var completionText = psJob.Name;
|
|
var listItemText = completionText;
|
|
|
|
if (CompletionRequiresQuotes(completionText, false))
|
|
{
|
|
var quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
completionText = quote + completionText + quote;
|
|
}
|
|
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
}
|
|
|
|
private static void NativeCompletionModuleCommands(
|
|
CompletionContext context,
|
|
string paramName,
|
|
List<CompletionResult> result,
|
|
bool loadedModulesOnly = false,
|
|
bool isImportModule = false,
|
|
bool skipEditionCheck = false)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (paramName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
if (isImportModule)
|
|
{
|
|
var moduleExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{ StringLiterals.PowerShellScriptFileExtension,
|
|
StringLiterals.PowerShellModuleFileExtension,
|
|
StringLiterals.PowerShellDataFileExtension,
|
|
StringLiterals.PowerShellNgenAssemblyExtension,
|
|
StringLiterals.PowerShellILAssemblyExtension,
|
|
StringLiterals.PowerShellILExecutableExtension,
|
|
StringLiterals.PowerShellCmdletizationFileExtension
|
|
};
|
|
var moduleFilesResults = new List<CompletionResult>(CompleteFilename(context, containerOnly: false, moduleExtensions));
|
|
if (moduleFilesResults.Count > 0)
|
|
result.AddRange(moduleFilesResults);
|
|
|
|
var assemblyOrModuleName = context.WordToComplete;
|
|
if (assemblyOrModuleName.IndexOfAny(Utils.Separators.DirectoryOrDrive) != -1)
|
|
{
|
|
// The partial input is a path, then we don't iterate modules under $ENV:PSModulePath
|
|
return;
|
|
}
|
|
}
|
|
|
|
var moduleResults = CompleteModuleName(context, loadedModulesOnly, skipEditionCheck);
|
|
if (moduleResults != null && moduleResults.Count > 0)
|
|
result.AddRange(moduleResults);
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
else if (paramName.Equals("Assembly", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
var moduleExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".dll" };
|
|
var moduleFilesResults = new List<CompletionResult>(CompleteFilename(context, /* containerOnly: */ false, moduleExtensions));
|
|
if (moduleFilesResults.Count > 0)
|
|
result.AddRange(moduleFilesResults);
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
}
|
|
|
|
private static void NativeCompletionProcessCommands(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName))
|
|
return;
|
|
|
|
var wordToComplete = context.WordToComplete ?? string.Empty;
|
|
var quote = HandleDoubleAndSingleQuote(ref wordToComplete);
|
|
|
|
if (!wordToComplete.EndsWith('*'))
|
|
{
|
|
wordToComplete += "*";
|
|
}
|
|
|
|
var powerShellExecutionHelper = context.Helper;
|
|
if (paramName.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Process");
|
|
}
|
|
else
|
|
{
|
|
powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Process").AddParameter("Name", wordToComplete);
|
|
}
|
|
|
|
Exception exceptionThrown;
|
|
var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
if (psObjects == null)
|
|
return;
|
|
|
|
if (paramName.Equals("Id", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
var pattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase);
|
|
foreach (dynamic process in psObjects)
|
|
{
|
|
var processId = process.Id.ToString();
|
|
if (pattern.IsMatch(processId))
|
|
{
|
|
var processName = process.Name;
|
|
|
|
var idAndName = $"{processId} - {processName}";
|
|
processId = quote + processId + quote;
|
|
result.Add(new CompletionResult(processId, idAndName, CompletionResultType.ParameterValue, idAndName));
|
|
}
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
else if (paramName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
var uniqueSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (dynamic process in psObjects)
|
|
{
|
|
var completionText = process.Name;
|
|
var listItemText = completionText;
|
|
|
|
if (uniqueSet.Contains(completionText))
|
|
continue;
|
|
|
|
uniqueSet.Add(completionText);
|
|
if (CompletionRequiresQuotes(completionText, false))
|
|
{
|
|
var quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
completionText = quote + completionText + quote;
|
|
}
|
|
|
|
// on macOS, system processes names will be empty if PowerShell isn't run as `sudo`
|
|
if (string.IsNullOrEmpty(listItemText))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
}
|
|
|
|
private static void NativeCompletionProviderCommands(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName) || !paramName.Equals("PSProvider", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
var providerName = context.WordToComplete ?? string.Empty;
|
|
var quote = HandleDoubleAndSingleQuote(ref providerName);
|
|
|
|
if (!providerName.EndsWith('*'))
|
|
{
|
|
providerName += "*";
|
|
}
|
|
|
|
var powerShellExecutionHelper = context.Helper;
|
|
powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-PSProvider").AddParameter("PSProvider", providerName);
|
|
var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out _);
|
|
if (psObjects == null)
|
|
return;
|
|
|
|
foreach (dynamic providerInfo in psObjects)
|
|
{
|
|
var completionText = providerInfo.Name;
|
|
var listItemText = completionText;
|
|
|
|
if (CompletionRequiresQuotes(completionText, false))
|
|
{
|
|
var quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
completionText = quote + completionText + quote;
|
|
}
|
|
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
|
|
private static void NativeCompletionDriveCommands(CompletionContext context, string psProvider, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName) || !paramName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
var wordToComplete = context.WordToComplete ?? string.Empty;
|
|
var quote = HandleDoubleAndSingleQuote(ref wordToComplete);
|
|
|
|
if (!wordToComplete.EndsWith('*'))
|
|
{
|
|
wordToComplete += "*";
|
|
}
|
|
|
|
var powerShellExecutionHelper = context.Helper;
|
|
var powershell = powerShellExecutionHelper
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-PSDrive")
|
|
.AddParameter("Name", wordToComplete);
|
|
if (psProvider != null)
|
|
powershell.AddParameter("PSProvider", psProvider);
|
|
|
|
var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out _);
|
|
if (psObjects != null)
|
|
{
|
|
foreach (dynamic driveInfo in psObjects)
|
|
{
|
|
var completionText = driveInfo.Name;
|
|
var listItemText = completionText;
|
|
|
|
if (CompletionRequiresQuotes(completionText, false))
|
|
{
|
|
var quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
completionText = quote + completionText + quote;
|
|
}
|
|
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
|
|
private static void NativeCompletionServiceCommands(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName))
|
|
return;
|
|
|
|
var wordToComplete = context.WordToComplete ?? string.Empty;
|
|
var quote = HandleDoubleAndSingleQuote(ref wordToComplete);
|
|
|
|
if (!wordToComplete.EndsWith('*'))
|
|
{
|
|
wordToComplete += "*";
|
|
}
|
|
|
|
Exception exceptionThrown;
|
|
var powerShellExecutionHelper = context.Helper;
|
|
if (paramName.Equals("DisplayName", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
powerShellExecutionHelper
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Service")
|
|
.AddParameter("DisplayName", wordToComplete)
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object")
|
|
.AddParameter("Property", "DisplayName");
|
|
var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
if (psObjects != null)
|
|
{
|
|
foreach (dynamic serviceInfo in psObjects)
|
|
{
|
|
var completionText = serviceInfo.DisplayName;
|
|
var listItemText = completionText;
|
|
|
|
if (CompletionRequiresQuotes(completionText, false))
|
|
{
|
|
var quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
completionText = quote + completionText + quote;
|
|
}
|
|
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
else if (paramName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Service").AddParameter("Name", wordToComplete);
|
|
var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
if (psObjects != null)
|
|
{
|
|
foreach (dynamic serviceInfo in psObjects)
|
|
{
|
|
var completionText = serviceInfo.Name;
|
|
var listItemText = completionText;
|
|
|
|
if (CompletionRequiresQuotes(completionText, false))
|
|
{
|
|
var quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
completionText = quote + completionText + quote;
|
|
}
|
|
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
}
|
|
|
|
private static void NativeCompletionVariableCommands(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName) || !paramName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
var variableName = context.WordToComplete ?? string.Empty;
|
|
var quote = HandleDoubleAndSingleQuote(ref variableName);
|
|
if (!variableName.EndsWith('*'))
|
|
{
|
|
variableName += "*";
|
|
}
|
|
|
|
var powerShellExecutionHelper = context.Helper;
|
|
var powershell = powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Get-Variable").AddParameter("Name", variableName);
|
|
var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out _);
|
|
if (psObjects == null)
|
|
return;
|
|
|
|
foreach (dynamic variable in psObjects)
|
|
{
|
|
var effectiveQuote = quote;
|
|
var completionText = variable.Name;
|
|
var listItemText = completionText;
|
|
|
|
// Handle special characters ? and * in variable names
|
|
if (completionText.IndexOfAny(Utils.Separators.StarOrQuestion) != -1)
|
|
{
|
|
effectiveQuote = "'";
|
|
completionText = completionText.Replace("?", "`?");
|
|
completionText = completionText.Replace("*", "`*");
|
|
}
|
|
|
|
if (!completionText.Equals("$", StringComparison.Ordinal) && CompletionRequiresQuotes(completionText, false))
|
|
{
|
|
var quoteInUse = effectiveQuote == string.Empty ? "'" : effectiveQuote;
|
|
if (quoteInUse == "'")
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
completionText = effectiveQuote + completionText + effectiveQuote;
|
|
}
|
|
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
|
|
private static void NativeCompletionAliasCommands(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName) ||
|
|
(!paramName.Equals("Definition", StringComparison.OrdinalIgnoreCase) &&
|
|
!paramName.Equals("Name", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
var powerShellExecutionHelper = context.Helper;
|
|
if (paramName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var commandName = context.WordToComplete ?? string.Empty;
|
|
var quote = HandleDoubleAndSingleQuote(ref commandName);
|
|
|
|
if (!commandName.EndsWith('*'))
|
|
{
|
|
commandName += "*";
|
|
}
|
|
|
|
Exception exceptionThrown;
|
|
var powershell = powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Get-Alias").AddParameter("Name", commandName);
|
|
var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
if (psObjects != null)
|
|
{
|
|
foreach (dynamic aliasInfo in psObjects)
|
|
{
|
|
var completionText = aliasInfo.Name;
|
|
var listItemText = completionText;
|
|
|
|
if (CompletionRequiresQuotes(completionText, false))
|
|
{
|
|
var quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
completionText = quote + completionText + quote;
|
|
}
|
|
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Complete for the parameter Definition
|
|
// Available commands
|
|
const CommandTypes commandTypes = CommandTypes.Cmdlet | CommandTypes.Function | CommandTypes.ExternalScript | CommandTypes.Configuration;
|
|
var commandResults = CompleteCommand(context, /* moduleName: */ null, commandTypes);
|
|
if (commandResults != null && commandResults.Count > 0)
|
|
result.AddRange(commandResults);
|
|
|
|
// The parameter Definition takes a file
|
|
var fileResults = new List<CompletionResult>(CompleteFilename(context));
|
|
if (fileResults.Count > 0)
|
|
result.AddRange(fileResults);
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
|
|
private static void NativeCompletionTraceSourceCommands(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName) || !paramName.Equals("Name", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
var traceSourceName = context.WordToComplete ?? string.Empty;
|
|
var quote = HandleDoubleAndSingleQuote(ref traceSourceName);
|
|
|
|
if (!traceSourceName.EndsWith('*'))
|
|
{
|
|
traceSourceName += "*";
|
|
}
|
|
|
|
var powerShellExecutionHelper = context.Helper;
|
|
var powershell = powerShellExecutionHelper.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Get-TraceSource").AddParameter("Name", traceSourceName);
|
|
Exception exceptionThrown;
|
|
var psObjects = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
if (psObjects == null)
|
|
return;
|
|
|
|
foreach (dynamic trace in psObjects)
|
|
{
|
|
var completionText = trace.Name;
|
|
var listItemText = completionText;
|
|
|
|
if (CompletionRequiresQuotes(completionText, false))
|
|
{
|
|
var quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else
|
|
{
|
|
completionText = quote + completionText + quote;
|
|
}
|
|
|
|
result.Add(new CompletionResult(completionText, listItemText, CompletionResultType.ParameterValue, listItemText));
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
|
|
private static void NativeCompletionSetLocationCommand(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName) ||
|
|
(!paramName.Equals("Path", StringComparison.OrdinalIgnoreCase) &&
|
|
!paramName.Equals("LiteralPath", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
context.WordToComplete ??= string.Empty;
|
|
var clearLiteralPath = false;
|
|
if (paramName.Equals("LiteralPath", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
clearLiteralPath = TurnOnLiteralPathOption(context);
|
|
}
|
|
|
|
try
|
|
{
|
|
var fileNameResults = CompleteFilename(context, containerOnly: true, extension: null);
|
|
if (fileNameResults != null)
|
|
result.AddRange(fileNameResults);
|
|
}
|
|
finally
|
|
{
|
|
if (clearLiteralPath)
|
|
context.Options.Remove("LiteralPaths");
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Provides completion results for NewItemCommand.
|
|
/// </summary>
|
|
/// <param name="context">Completion context.</param>
|
|
/// <param name="paramName">Name of the parameter whose value needs completion.</param>
|
|
/// <param name="result">List of completion suggestions.</param>
|
|
private static void NativeCompletionNewItemCommand(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var executionContext = context.ExecutionContext;
|
|
|
|
var boundArgs = GetBoundArgumentsAsHashtable(context);
|
|
var providedPath = boundArgs["Path"] as string ?? executionContext.SessionState.Path.CurrentLocation.Path;
|
|
|
|
ProviderInfo provider;
|
|
executionContext.LocationGlobber.GetProviderPath(providedPath, out provider);
|
|
|
|
var isFileSystem = provider != null &&
|
|
provider.Name.Equals(FileSystemProvider.ProviderName, StringComparison.OrdinalIgnoreCase);
|
|
|
|
// AutoComplete only if filesystem provider.
|
|
if (isFileSystem)
|
|
{
|
|
if (paramName.Equals("ItemType", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (!string.IsNullOrEmpty(context.WordToComplete))
|
|
{
|
|
WildcardPattern patternEvaluator = WildcardPattern.Get(context.WordToComplete + "*", WildcardOptions.IgnoreCase);
|
|
|
|
if (patternEvaluator.IsMatch("file"))
|
|
{
|
|
result.Add(new CompletionResult("File"));
|
|
}
|
|
else if (patternEvaluator.IsMatch("directory"))
|
|
{
|
|
result.Add(new CompletionResult("Directory"));
|
|
}
|
|
else if (patternEvaluator.IsMatch("symboliclink"))
|
|
{
|
|
result.Add(new CompletionResult("SymbolicLink"));
|
|
}
|
|
else if (patternEvaluator.IsMatch("junction"))
|
|
{
|
|
result.Add(new CompletionResult("Junction"));
|
|
}
|
|
else if (patternEvaluator.IsMatch("hardlink"))
|
|
{
|
|
result.Add(new CompletionResult("HardLink"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result.Add(new CompletionResult("File"));
|
|
result.Add(new CompletionResult("Directory"));
|
|
result.Add(new CompletionResult("SymbolicLink"));
|
|
result.Add(new CompletionResult("Junction"));
|
|
result.Add(new CompletionResult("HardLink"));
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void NativeCompletionCopyMoveItemCommand(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (paramName.Equals("LiteralPath", StringComparison.OrdinalIgnoreCase) || paramName.Equals("Path", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
NativeCompletionPathArgument(context, paramName, result);
|
|
}
|
|
else if (paramName.Equals("Destination", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// The parameter Destination for Move-Item and Copy-Item takes literal path
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
context.WordToComplete ??= string.Empty;
|
|
var clearLiteralPath = TurnOnLiteralPathOption(context);
|
|
|
|
try
|
|
{
|
|
var fileNameResults = CompleteFilename(context);
|
|
if (fileNameResults != null)
|
|
result.AddRange(fileNameResults);
|
|
}
|
|
finally
|
|
{
|
|
if (clearLiteralPath)
|
|
context.Options.Remove("LiteralPaths");
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
}
|
|
|
|
private static void NativeCompletionPathArgument(CompletionContext context, string paramName, List<CompletionResult> result)
|
|
{
|
|
if (string.IsNullOrEmpty(paramName) ||
|
|
(!paramName.Equals("LiteralPath", StringComparison.OrdinalIgnoreCase) &&
|
|
(!paramName.Equals("Path", StringComparison.OrdinalIgnoreCase)) &&
|
|
(!paramName.Equals("FilePath", StringComparison.OrdinalIgnoreCase))))
|
|
{
|
|
return;
|
|
}
|
|
|
|
RemoveLastNullCompletionResult(result);
|
|
|
|
context.WordToComplete ??= string.Empty;
|
|
var clearLiteralPath = false;
|
|
if (paramName.Equals("LiteralPath", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
clearLiteralPath = TurnOnLiteralPathOption(context);
|
|
}
|
|
|
|
try
|
|
{
|
|
var fileNameResults = CompleteFilename(context);
|
|
if (fileNameResults != null)
|
|
result.AddRange(fileNameResults);
|
|
}
|
|
finally
|
|
{
|
|
if (clearLiteralPath)
|
|
context.Options.Remove("LiteralPaths");
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
|
|
private static IEnumerable<PSTypeName> GetInferenceTypes(CompletionContext context, CommandAst commandAst)
|
|
{
|
|
// Command is something like where-object/foreach-object/format-list/etc. where there is a parameter that is a property name
|
|
// and we want member names based on the input object, which is either the parameter InputObject, or comes from the pipeline.
|
|
if (commandAst.Parent is not PipelineAst pipelineAst)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
int i;
|
|
for (i = 0; i < pipelineAst.PipelineElements.Count; i++)
|
|
{
|
|
if (pipelineAst.PipelineElements[i] == commandAst)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
IEnumerable<PSTypeName> prevType = null;
|
|
if (i == 0)
|
|
{
|
|
// based on a type of the argument which is binded to 'InputObject' parameter.
|
|
AstParameterArgumentPair pair;
|
|
if (!context.PseudoBindingInfo.BoundArguments.TryGetValue("InputObject", out pair)
|
|
|| !pair.ArgumentSpecified)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var astPair = pair as AstPair;
|
|
if (astPair == null || astPair.Argument == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
prevType = AstTypeInference.InferTypeOf(astPair.Argument, context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval);
|
|
}
|
|
else
|
|
{
|
|
// based on OutputTypeAttribute() of the first cmdlet in pipeline.
|
|
prevType = AstTypeInference.InferTypeOf(pipelineAst.PipelineElements[i - 1], context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval);
|
|
}
|
|
|
|
return prevType;
|
|
}
|
|
|
|
private static void NativeCompletionMemberName(CompletionContext context, List<CompletionResult> result, CommandAst commandAst)
|
|
{
|
|
IEnumerable<PSTypeName> prevType = GetInferenceTypes(context, commandAst);
|
|
if (prevType is not null)
|
|
{
|
|
CompleteMemberByInferredType(context.TypeInferenceContext, prevType, result, context.WordToComplete + "*", filter: IsPropertyMember, isStatic: false);
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
|
|
private static void NativeCompletionFormatViewName(
|
|
CompletionContext context,
|
|
Dictionary<string, AstParameterArgumentPair> boundArguments,
|
|
List<CompletionResult> result,
|
|
CommandAst commandAst,
|
|
string commandName)
|
|
{
|
|
IEnumerable<PSTypeName> prevType = NativeCommandArgumentCompletion_InferTypesOfArgument(boundArguments, commandAst, context, "InputObject");
|
|
|
|
if (prevType is not null)
|
|
{
|
|
string[] inferTypeNames = prevType.Select(t => t.Name).ToArray();
|
|
CompleteFormatViewByInferredType(context, inferTypeNames, result, commandName);
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
|
|
private static void NativeCompletionTypeName(CompletionContext context, List<CompletionResult> result)
|
|
{
|
|
var wordToComplete = context.WordToComplete;
|
|
var isQuoted = wordToComplete.Length > 0 && (wordToComplete[0].IsSingleQuote() || wordToComplete[0].IsDoubleQuote());
|
|
string prefix = string.Empty;
|
|
string suffix = string.Empty;
|
|
if (isQuoted)
|
|
{
|
|
prefix = suffix = wordToComplete.Substring(0, 1);
|
|
|
|
var endQuoted = (wordToComplete.Length > 1) && wordToComplete[wordToComplete.Length - 1] == wordToComplete[0];
|
|
wordToComplete = wordToComplete.Substring(1, wordToComplete.Length - (endQuoted ? 2 : 1));
|
|
}
|
|
|
|
if (wordToComplete.Contains('['))
|
|
{
|
|
var cursor = (InternalScriptPosition)context.CursorPosition;
|
|
cursor = cursor.CloneWithNewOffset(cursor.Offset - context.TokenAtCursor.Extent.StartOffset - (isQuoted ? 1 : 0));
|
|
var fullTypeName = Parser.ScanType(wordToComplete, ignoreErrors: true);
|
|
var typeNameToComplete = CompletionAnalysis.FindTypeNameToComplete(fullTypeName, cursor);
|
|
if (typeNameToComplete == null)
|
|
return;
|
|
|
|
var openBrackets = 0;
|
|
var closeBrackets = 0;
|
|
foreach (char c in wordToComplete)
|
|
{
|
|
if (c == '[') openBrackets += 1;
|
|
else if (c == ']') closeBrackets += 1;
|
|
}
|
|
|
|
wordToComplete = typeNameToComplete.FullName;
|
|
var typeNameText = fullTypeName.Extent.Text;
|
|
if (!isQuoted)
|
|
{
|
|
// We need to add quotes - the square bracket messes up parsing the argument
|
|
prefix = suffix = "'";
|
|
}
|
|
|
|
if (closeBrackets < openBrackets)
|
|
{
|
|
suffix = suffix.Insert(0, new string(']', (openBrackets - closeBrackets)));
|
|
}
|
|
|
|
if (isQuoted && closeBrackets == openBrackets)
|
|
{
|
|
// Already quoted, and has matching []. We can give a better Intellisense experience
|
|
// if we only replace the minimum.
|
|
context.ReplacementIndex = typeNameToComplete.Extent.StartOffset + context.TokenAtCursor.Extent.StartOffset + 1;
|
|
context.ReplacementLength = wordToComplete.Length;
|
|
prefix = suffix = string.Empty;
|
|
}
|
|
else
|
|
{
|
|
prefix += typeNameText.Substring(0, typeNameToComplete.Extent.StartOffset);
|
|
suffix = suffix.Insert(0, typeNameText.Substring(typeNameToComplete.Extent.EndOffset));
|
|
}
|
|
}
|
|
|
|
context.WordToComplete = wordToComplete;
|
|
|
|
var typeResults = CompleteType(context, prefix, suffix);
|
|
if (typeResults != null)
|
|
{
|
|
result.AddRange(typeResults);
|
|
}
|
|
|
|
result.Add(CompletionResult.Null);
|
|
}
|
|
|
|
#endregion Native Command Argument Completion
|
|
|
|
/// <summary>
|
|
/// Find the positional argument at the specific position from the parsed argument list.
|
|
/// </summary>
|
|
/// <param name="parsedArguments"></param>
|
|
/// <param name="position"></param>
|
|
/// <param name="lastPositionalArgument"></param>
|
|
/// <returns>
|
|
/// If the command line after the [tab] will not be truncated, the return value could be non-null: Get-Cmdlet [tab] abc
|
|
/// If the command line after the [tab] is truncated, the return value will always be null
|
|
/// </returns>
|
|
private static AstPair FindTargetPositionalArgument(Collection<AstParameterArgumentPair> parsedArguments, int position, out AstPair lastPositionalArgument)
|
|
{
|
|
int index = 0;
|
|
lastPositionalArgument = null;
|
|
foreach (AstParameterArgumentPair pair in parsedArguments)
|
|
{
|
|
if (!pair.ParameterSpecified && index == position)
|
|
return (AstPair)pair;
|
|
else if (!pair.ParameterSpecified)
|
|
{
|
|
index++;
|
|
lastPositionalArgument = (AstPair)pair;
|
|
}
|
|
}
|
|
|
|
// Cannot find an existing positional argument at 'position'
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the location where 'tab' is typed based on the line and column.
|
|
/// </summary>
|
|
private static ArgumentLocation FindTargetArgumentLocation(Collection<AstParameterArgumentPair> parsedArguments, Token token)
|
|
{
|
|
int position = 0;
|
|
AstParameterArgumentPair prevArg = null;
|
|
foreach (AstParameterArgumentPair pair in parsedArguments)
|
|
{
|
|
switch (pair.ParameterArgumentType)
|
|
{
|
|
case AstParameterArgumentType.AstPair:
|
|
{
|
|
var arg = (AstPair)pair;
|
|
if (arg.ParameterSpecified)
|
|
{
|
|
// Named argument
|
|
if (arg.Parameter.Extent.StartOffset > token.Extent.StartOffset)
|
|
{
|
|
// case: Get-Cmdlet <tab> -Param abc
|
|
return GenerateArgumentLocation(prevArg, position);
|
|
}
|
|
|
|
if (!arg.ParameterContainsArgument && arg.Argument.Extent.StartOffset > token.Extent.StartOffset)
|
|
{
|
|
// case: Get-Cmdlet -Param <tab> abc
|
|
return new ArgumentLocation() { Argument = arg, IsPositional = false, Position = -1 };
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Positional argument
|
|
if (arg.Argument.Extent.StartOffset > token.Extent.StartOffset)
|
|
{
|
|
// case: Get-Cmdlet <tab> abc
|
|
return GenerateArgumentLocation(prevArg, position);
|
|
}
|
|
|
|
position++;
|
|
}
|
|
|
|
prevArg = arg;
|
|
}
|
|
|
|
break;
|
|
case AstParameterArgumentType.Fake:
|
|
case AstParameterArgumentType.Switch:
|
|
{
|
|
if (pair.Parameter.Extent.StartOffset > token.Extent.StartOffset)
|
|
{
|
|
return GenerateArgumentLocation(prevArg, position);
|
|
}
|
|
|
|
prevArg = pair;
|
|
}
|
|
|
|
break;
|
|
case AstParameterArgumentType.AstArray:
|
|
case AstParameterArgumentType.PipeObject:
|
|
Diagnostics.Assert(false, "parsed arguments should not contain AstArray and PipeObject");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// The 'tab' should be typed after the last argument
|
|
return GenerateArgumentLocation(prevArg, position);
|
|
}
|
|
|
|
/// <summary>
|
|
/// </summary>
|
|
/// <param name="prev">The argument that is right before the 'tab' location.</param>
|
|
/// <param name="position">The number of positional arguments before the 'tab' location.</param>
|
|
/// <returns></returns>
|
|
private static ArgumentLocation GenerateArgumentLocation(AstParameterArgumentPair prev, int position)
|
|
{
|
|
// Tab is typed before the first argument
|
|
if (prev == null)
|
|
{
|
|
return new ArgumentLocation() { Argument = null, IsPositional = true, Position = 0 };
|
|
}
|
|
|
|
switch (prev.ParameterArgumentType)
|
|
{
|
|
case AstParameterArgumentType.AstPair:
|
|
case AstParameterArgumentType.Switch:
|
|
if (!prev.ParameterSpecified)
|
|
return new ArgumentLocation() { Argument = null, IsPositional = true, Position = position };
|
|
|
|
return prev.Parameter.Extent.Text.EndsWith(':')
|
|
? new ArgumentLocation() { Argument = prev, IsPositional = false, Position = -1 }
|
|
|
|
: new ArgumentLocation() { Argument = null, IsPositional = true, Position = position };
|
|
case AstParameterArgumentType.Fake:
|
|
return new ArgumentLocation() { Argument = prev, IsPositional = false, Position = -1 };
|
|
default:
|
|
Diagnostics.Assert(false, "parsed arguments should not contain AstArray and PipeObject");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the location where 'tab' is typed based on the expressionAst.
|
|
/// </summary>
|
|
/// <param name="parsedArguments"></param>
|
|
/// <param name="expAst"></param>
|
|
/// <returns></returns>
|
|
private static ArgumentLocation FindTargetArgumentLocation(Collection<AstParameterArgumentPair> parsedArguments, ExpressionAst expAst)
|
|
{
|
|
Diagnostics.Assert(expAst != null, "Caller needs to make sure expAst is not null");
|
|
int position = 0;
|
|
foreach (AstParameterArgumentPair pair in parsedArguments)
|
|
{
|
|
switch (pair.ParameterArgumentType)
|
|
{
|
|
case AstParameterArgumentType.AstPair:
|
|
{
|
|
AstPair arg = (AstPair)pair;
|
|
if (arg.ArgumentIsCommandParameterAst)
|
|
continue;
|
|
|
|
if (arg.ParameterContainsArgument && arg.Argument == expAst)
|
|
{
|
|
return new ArgumentLocation() { IsPositional = false, Position = -1, Argument = arg };
|
|
}
|
|
|
|
if (arg.Argument.GetHashCode() == expAst.GetHashCode())
|
|
{
|
|
return arg.ParameterSpecified ?
|
|
new ArgumentLocation() { IsPositional = false, Position = -1, Argument = arg } :
|
|
new ArgumentLocation() { IsPositional = true, Position = position, Argument = arg };
|
|
}
|
|
|
|
if (!arg.ParameterSpecified)
|
|
position++;
|
|
}
|
|
|
|
break;
|
|
case AstParameterArgumentType.Fake:
|
|
case AstParameterArgumentType.Switch:
|
|
// FakePair and SwitchPair contains no ExpressionAst
|
|
break;
|
|
case AstParameterArgumentType.AstArray:
|
|
case AstParameterArgumentType.PipeObject:
|
|
Diagnostics.Assert(false, "parsed arguments should not contain AstArray and PipeObject arguments");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We should be able to find the ExpAst from the parsed argument list, if all parameters was specified correctly.
|
|
// We may try to complete something incorrect
|
|
// ls -Recurse -QQQ qwe<+tab>
|
|
return null;
|
|
}
|
|
|
|
private sealed class ArgumentLocation
|
|
{
|
|
internal bool IsPositional { get; set; }
|
|
|
|
internal int Position { get; set; }
|
|
|
|
internal AstParameterArgumentPair Argument { get; set; }
|
|
}
|
|
|
|
#endregion Command Arguments
|
|
|
|
#region Filenames
|
|
|
|
/// <summary>
|
|
/// </summary>
|
|
/// <param name="fileName"></param>
|
|
/// <returns></returns>
|
|
[SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly")]
|
|
public static IEnumerable<CompletionResult> CompleteFilename(string fileName)
|
|
{
|
|
var runspace = Runspace.DefaultRunspace;
|
|
if (runspace == null)
|
|
{
|
|
// No runspace, just return no results.
|
|
return CommandCompletion.EmptyCompletionResult;
|
|
}
|
|
|
|
var helper = new PowerShellExecutionHelper(PowerShell.Create(RunspaceMode.CurrentRunspace));
|
|
var executionContext = helper.CurrentPowerShell.Runspace.ExecutionContext;
|
|
return CompleteFilename(new CompletionContext { WordToComplete = fileName, Helper = helper, ExecutionContext = executionContext });
|
|
}
|
|
|
|
internal static IEnumerable<CompletionResult> CompleteFilename(CompletionContext context)
|
|
{
|
|
return CompleteFilename(context, containerOnly: false, extension: null);
|
|
}
|
|
|
|
[SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly")]
|
|
internal static IEnumerable<CompletionResult> CompleteFilename(CompletionContext context, bool containerOnly, HashSet<string> extension)
|
|
{
|
|
var wordToComplete = context.WordToComplete;
|
|
var quote = HandleDoubleAndSingleQuote(ref wordToComplete);
|
|
var results = new List<CompletionResult>();
|
|
|
|
// First, try to match \\server\share
|
|
var shareMatch = Regex.Match(wordToComplete, "^\\\\\\\\([^\\\\]+)\\\\([^\\\\]*)$");
|
|
if (shareMatch.Success)
|
|
{
|
|
// Only match share names, no filenames.
|
|
var server = shareMatch.Groups[1].Value;
|
|
var sharePattern = WildcardPattern.Get(shareMatch.Groups[2].Value + "*", WildcardOptions.IgnoreCase);
|
|
var ignoreHidden = context.GetOption("IgnoreHiddenShares", @default: false);
|
|
var shares = GetFileShares(server, ignoreHidden);
|
|
foreach (var share in shares)
|
|
{
|
|
if (sharePattern.IsMatch(share))
|
|
{
|
|
string shareFullPath = "\\\\" + server + "\\" + share;
|
|
if (quote != string.Empty)
|
|
{
|
|
shareFullPath = quote + shareFullPath + quote;
|
|
}
|
|
|
|
results.Add(new CompletionResult(shareFullPath, shareFullPath, CompletionResultType.ProviderContainer, shareFullPath));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We want to prefer relative paths in a completion result unless the user has already
|
|
// specified a drive or portion of the path.
|
|
var executionContext = context.ExecutionContext;
|
|
var defaultRelative = string.IsNullOrWhiteSpace(wordToComplete)
|
|
|| (wordToComplete.IndexOfAny(Utils.Separators.Directory) != 0 &&
|
|
!Regex.Match(wordToComplete, @"^~[\\/]+.*").Success &&
|
|
!executionContext.LocationGlobber.IsAbsolutePath(wordToComplete, out _));
|
|
var relativePaths = context.GetOption("RelativePaths", @default: defaultRelative);
|
|
var useLiteralPath = context.GetOption("LiteralPaths", @default: false);
|
|
|
|
if (useLiteralPath && LocationGlobber.StringContainsGlobCharacters(wordToComplete))
|
|
{
|
|
wordToComplete = WildcardPattern.Escape(wordToComplete, Utils.Separators.StarOrQuestion);
|
|
}
|
|
|
|
if (!defaultRelative && wordToComplete.Length >= 2 && wordToComplete[1] == ':' && char.IsLetter(wordToComplete[0]) && executionContext != null)
|
|
{
|
|
// We don't actually need the drive, but the drive must be "mounted" in PowerShell before completion
|
|
// can succeed. This call will mount the drive if it wasn't already.
|
|
executionContext.SessionState.Drive.GetAtScope(wordToComplete.Substring(0, 1), "global");
|
|
}
|
|
|
|
var powerShellExecutionHelper = context.Helper;
|
|
powerShellExecutionHelper
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Resolve-Path")
|
|
.AddParameter("Path", wordToComplete + "*");
|
|
|
|
Exception exceptionThrown;
|
|
var psobjs = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
|
|
if (psobjs != null)
|
|
{
|
|
var isFileSystem = false;
|
|
var wordContainsProviderId = ProviderSpecified(wordToComplete);
|
|
|
|
if (psobjs.Count > 0)
|
|
{
|
|
dynamic firstObj = psobjs[0];
|
|
var provider = firstObj.Provider as ProviderInfo;
|
|
isFileSystem = provider != null &&
|
|
provider.Name.Equals(FileSystemProvider.ProviderName,
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
ProviderInfo provider;
|
|
if (defaultRelative)
|
|
{
|
|
provider = executionContext.EngineSessionState.CurrentDrive.Provider;
|
|
}
|
|
else
|
|
{
|
|
executionContext.LocationGlobber.GetProviderPath(wordToComplete, out provider);
|
|
}
|
|
|
|
isFileSystem = provider != null &&
|
|
provider.Name.Equals(FileSystemProvider.ProviderName,
|
|
StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
if (isFileSystem)
|
|
{
|
|
bool hiddenFilesAreHandled = false;
|
|
|
|
if (psobjs.Count > 0 && !LocationGlobber.StringContainsGlobCharacters(wordToComplete))
|
|
{
|
|
string leaf = null;
|
|
string pathWithoutProvider = wordContainsProviderId
|
|
? wordToComplete.Substring(wordToComplete.IndexOf(':') + 2)
|
|
: wordToComplete;
|
|
|
|
try
|
|
{
|
|
leaf = Path.GetFileName(pathWithoutProvider);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
|
|
var notHiddenEntries = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
string providerPath = null;
|
|
|
|
foreach (dynamic entry in psobjs)
|
|
{
|
|
providerPath = entry.ProviderPath;
|
|
if (string.IsNullOrEmpty(providerPath))
|
|
{
|
|
// This is unexpected. ProviderPath should never be null or an empty string
|
|
leaf = null;
|
|
break;
|
|
}
|
|
|
|
if (!notHiddenEntries.Contains(providerPath))
|
|
{
|
|
notHiddenEntries.Add(providerPath);
|
|
}
|
|
}
|
|
|
|
if (leaf != null)
|
|
{
|
|
leaf += "*";
|
|
var parentPath = Path.GetDirectoryName(providerPath);
|
|
|
|
// ProviderPath should be absolute path for FileSystem entries
|
|
if (!string.IsNullOrEmpty(parentPath))
|
|
{
|
|
string[] entries = null;
|
|
try
|
|
{
|
|
entries = Directory.GetFileSystemEntries(parentPath, leaf, _enumerationOptions);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
|
|
if (entries != null)
|
|
{
|
|
hiddenFilesAreHandled = true;
|
|
|
|
if (entries.Length > notHiddenEntries.Count)
|
|
{
|
|
// Do the iteration only if there are hidden files
|
|
foreach (var entry in entries)
|
|
{
|
|
if (notHiddenEntries.Contains(entry))
|
|
continue;
|
|
|
|
var fileInfo = new FileInfo(entry);
|
|
try
|
|
{
|
|
if ((fileInfo.Attributes & FileAttributes.Hidden) != 0)
|
|
{
|
|
PSObject wrapper = PSObject.AsPSObject(entry);
|
|
psobjs.Add(wrapper);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// do nothing if can't get file attributes
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hiddenFilesAreHandled)
|
|
{
|
|
powerShellExecutionHelper
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-ChildItem")
|
|
.AddParameter("Path", wordToComplete + "*")
|
|
.AddParameter("Hidden", true);
|
|
|
|
var hiddenItems = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
if (hiddenItems != null && hiddenItems.Count > 0)
|
|
{
|
|
foreach (var hiddenItem in hiddenItems)
|
|
{
|
|
psobjs.Add(hiddenItem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sorting the results by the path
|
|
var sortedPsobjs = psobjs.OrderBy(static a => a, new ItemPathComparer());
|
|
|
|
foreach (PSObject psobj in sortedPsobjs)
|
|
{
|
|
object baseObj = PSObject.Base(psobj);
|
|
string path = null, providerPath = null;
|
|
|
|
// Get the path, the PSObject could be:
|
|
// 1. a PathInfo object -- results of Resolve-Path
|
|
// 2. a FileSystemInfo Object -- results of Get-ChildItem
|
|
// 3. a string -- the path results return by the direct .NET API invocation
|
|
var baseObjAsPathInfo = baseObj as PathInfo;
|
|
if (baseObjAsPathInfo != null)
|
|
{
|
|
path = baseObjAsPathInfo.Path;
|
|
providerPath = baseObjAsPathInfo.ProviderPath;
|
|
}
|
|
else if (baseObj is FileSystemInfo)
|
|
{
|
|
// The target provider is the FileSystem
|
|
dynamic dirResult = psobj;
|
|
providerPath = dirResult.FullName;
|
|
path = wordContainsProviderId ? dirResult.PSPath : providerPath;
|
|
}
|
|
else
|
|
{
|
|
var baseObjAsString = baseObj as string;
|
|
if (baseObjAsString != null)
|
|
{
|
|
// The target provider is the FileSystem
|
|
providerPath = baseObjAsString;
|
|
path = wordContainsProviderId
|
|
? FileSystemProvider.ProviderName + "::" + baseObjAsString
|
|
: providerPath;
|
|
}
|
|
}
|
|
|
|
if (path == null) continue;
|
|
if (isFileSystem && providerPath == null) continue;
|
|
|
|
string completionText;
|
|
if (relativePaths)
|
|
{
|
|
try
|
|
{
|
|
var sessionStateInternal = executionContext.EngineSessionState;
|
|
completionText = sessionStateInternal.NormalizeRelativePath(path, sessionStateInternal.CurrentLocation.ProviderPath);
|
|
string parentDirectory = ".." + StringLiterals.DefaultPathSeparator;
|
|
if (!completionText.StartsWith(parentDirectory, StringComparison.Ordinal))
|
|
completionText = Path.Combine(".", completionText);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// The object at the specified path is not accessable, such as c:\hiberfil.sys (for hibernation) or c:\pagefile.sys (for paging)
|
|
// We ignore those files
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
completionText = path;
|
|
}
|
|
|
|
if (ProviderSpecified(completionText) && !wordContainsProviderId)
|
|
{
|
|
// Remove the provider id from the path: cd \\scratch2\scratch\dongbw
|
|
var index = completionText.IndexOf(':');
|
|
completionText = completionText.Substring(index + 2);
|
|
}
|
|
|
|
if (CompletionRequiresQuotes(completionText, !useLiteralPath))
|
|
{
|
|
var quoteInUse = quote == string.Empty ? "'" : quote;
|
|
if (quoteInUse == "'")
|
|
{
|
|
completionText = completionText.Replace("'", "''");
|
|
}
|
|
else
|
|
{
|
|
// When double quote is in use, we have to escape the backtip and '$' even when using literal path
|
|
// Get-Content -LiteralPath ".\a``g.txt"
|
|
completionText = completionText.Replace("`", "``");
|
|
completionText = completionText.Replace("$", "`$");
|
|
}
|
|
|
|
if (!useLiteralPath)
|
|
{
|
|
if (quoteInUse == "'")
|
|
{
|
|
completionText = completionText.Replace("[", "`[");
|
|
completionText = completionText.Replace("]", "`]");
|
|
}
|
|
else
|
|
{
|
|
completionText = completionText.Replace("[", "``[");
|
|
completionText = completionText.Replace("]", "``]");
|
|
}
|
|
}
|
|
|
|
completionText = quoteInUse + completionText + quoteInUse;
|
|
}
|
|
else if (quote != string.Empty)
|
|
{
|
|
completionText = quote + completionText + quote;
|
|
}
|
|
|
|
if (isFileSystem)
|
|
{
|
|
// Use .NET APIs directly to reduce the time overhead
|
|
var isContainer = Directory.Exists(providerPath);
|
|
if (containerOnly && !isContainer)
|
|
continue;
|
|
|
|
if (!containerOnly && !isContainer && !CheckFileExtension(providerPath, extension))
|
|
continue;
|
|
|
|
string tooltip = providerPath, listItemText = Path.GetFileName(providerPath);
|
|
results.Add(new CompletionResult(completionText, listItemText,
|
|
isContainer ? CompletionResultType.ProviderContainer : CompletionResultType.ProviderItem,
|
|
tooltip));
|
|
}
|
|
else
|
|
{
|
|
powerShellExecutionHelper
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Item")
|
|
.AddParameter("LiteralPath", path);
|
|
var items = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
if (items != null && items.Count == 1)
|
|
{
|
|
dynamic item = items[0];
|
|
var isContainer = LanguagePrimitives.ConvertTo<bool>(item.PSIsContainer);
|
|
|
|
if (containerOnly && !isContainer)
|
|
continue;
|
|
|
|
powerShellExecutionHelper
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Convert-Path")
|
|
.AddParameter("LiteralPath", item.PSPath);
|
|
var tooltips = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
string tooltip = null, listItemText = item.PSChildName;
|
|
if (tooltips != null && tooltips.Count == 1)
|
|
{
|
|
tooltip = PSObject.Base(tooltips[0]) as string;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(listItemText))
|
|
{
|
|
// For provider items that don't have PSChildName values, such as variable::error
|
|
listItemText = item.Name;
|
|
}
|
|
|
|
results.Add(new CompletionResult(completionText, listItemText,
|
|
isContainer ? CompletionResultType.ProviderContainer : CompletionResultType.ProviderItem,
|
|
tooltip ?? path));
|
|
}
|
|
else
|
|
{
|
|
// We can get here when get-item fails, perhaps due an acl or whatever.
|
|
results.Add(new CompletionResult(completionText));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
private struct SHARE_INFO_1
|
|
{
|
|
public string netname;
|
|
public int type;
|
|
public string remark;
|
|
}
|
|
|
|
private const int MAX_PREFERRED_LENGTH = -1;
|
|
private const int NERR_Success = 0;
|
|
private const int ERROR_MORE_DATA = 234;
|
|
private const int STYPE_DISKTREE = 0;
|
|
private const int STYPE_MASK = 0x000000FF;
|
|
|
|
private static readonly System.IO.EnumerationOptions _enumerationOptions = new System.IO.EnumerationOptions
|
|
{
|
|
MatchCasing = MatchCasing.CaseInsensitive,
|
|
AttributesToSkip = 0 // Default is to skip Hidden and System files, so we clear this to retain existing behavior
|
|
};
|
|
|
|
[DllImport("Netapi32.dll", CharSet = CharSet.Unicode)]
|
|
private static extern int NetShareEnum(string serverName, int level, out IntPtr bufptr, int prefMaxLen,
|
|
out uint entriesRead, out uint totalEntries, ref uint resumeHandle);
|
|
|
|
internal static List<string> GetFileShares(string machine, bool ignoreHidden)
|
|
{
|
|
#if UNIX
|
|
return new List<string>();
|
|
#else
|
|
IntPtr shBuf;
|
|
uint numEntries;
|
|
uint totalEntries;
|
|
uint resumeHandle = 0;
|
|
int result = NetShareEnum(machine, 1, out shBuf,
|
|
MAX_PREFERRED_LENGTH, out numEntries, out totalEntries,
|
|
ref resumeHandle);
|
|
|
|
var shares = new List<string>();
|
|
if (result == NERR_Success || result == ERROR_MORE_DATA)
|
|
{
|
|
for (int i = 0; i < numEntries; ++i)
|
|
{
|
|
IntPtr curInfoPtr = (IntPtr)((long)shBuf + (Marshal.SizeOf<SHARE_INFO_1>() * i));
|
|
SHARE_INFO_1 shareInfo = Marshal.PtrToStructure<SHARE_INFO_1>(curInfoPtr);
|
|
|
|
if ((shareInfo.type & STYPE_MASK) != STYPE_DISKTREE)
|
|
continue;
|
|
if (ignoreHidden && shareInfo.netname.EndsWith('$'))
|
|
continue;
|
|
shares.Add(shareInfo.netname);
|
|
}
|
|
}
|
|
|
|
return shares;
|
|
#endif
|
|
}
|
|
|
|
private static bool CheckFileExtension(string path, HashSet<string> extension)
|
|
{
|
|
if (extension == null || extension.Count == 0)
|
|
return true;
|
|
|
|
var ext = System.IO.Path.GetExtension(path);
|
|
return ext == null || extension.Contains(ext);
|
|
}
|
|
|
|
#endregion Filenames
|
|
|
|
#region Variable
|
|
|
|
/// <summary>
|
|
/// </summary>
|
|
/// <param name="variableName"></param>
|
|
/// <returns></returns>
|
|
public static IEnumerable<CompletionResult> CompleteVariable(string variableName)
|
|
{
|
|
var runspace = Runspace.DefaultRunspace;
|
|
if (runspace == null)
|
|
{
|
|
// No runspace, just return no results.
|
|
return CommandCompletion.EmptyCompletionResult;
|
|
}
|
|
|
|
var helper = new PowerShellExecutionHelper(PowerShell.Create(RunspaceMode.CurrentRunspace));
|
|
var executionContext = helper.CurrentPowerShell.Runspace.ExecutionContext;
|
|
return CompleteVariable(new CompletionContext { WordToComplete = variableName, Helper = helper, ExecutionContext = executionContext });
|
|
}
|
|
|
|
private static readonly string[] s_variableScopes = new string[] { "Global:", "Local:", "Script:", "Private:" };
|
|
|
|
private static readonly char[] s_charactersRequiringQuotes = new char[] {
|
|
'-', '`', '&', '@', '\'', '"', '#', '{', '}', '(', ')', '$', ',', ';', '|', '<', '>', ' ', '.', '\\', '/', '\t', '^',
|
|
};
|
|
|
|
internal static List<CompletionResult> CompleteVariable(CompletionContext context)
|
|
{
|
|
HashSet<string> hashedResults = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
List<CompletionResult> results = new List<CompletionResult>();
|
|
|
|
var wordToComplete = context.WordToComplete;
|
|
var colon = wordToComplete.IndexOf(':');
|
|
|
|
var lastAst = context.RelatedAsts?.Last();
|
|
var variableAst = lastAst as VariableExpressionAst;
|
|
var prefix = variableAst != null && variableAst.Splatted ? "@" : "$";
|
|
|
|
// Look for variables in the input (e.g. parameters, etc.) before checking session state - these
|
|
// variables might not exist in session state yet.
|
|
var wildcardPattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase);
|
|
if (lastAst != null)
|
|
{
|
|
Ast parent = lastAst.Parent;
|
|
var findVariablesVisitor = new FindVariablesVisitor { CompletionVariableAst = lastAst };
|
|
while (parent != null)
|
|
{
|
|
if (parent is IParameterMetadataProvider)
|
|
{
|
|
findVariablesVisitor.Top = parent;
|
|
parent.Visit(findVariablesVisitor);
|
|
}
|
|
|
|
parent = parent.Parent;
|
|
}
|
|
|
|
foreach (Tuple<string, Ast> varAst in findVariablesVisitor.VariableSources)
|
|
{
|
|
Ast astTarget = null;
|
|
string userPath = null;
|
|
|
|
VariableExpressionAst variableDefinitionAst = varAst.Item2 as VariableExpressionAst;
|
|
if (variableDefinitionAst != null)
|
|
{
|
|
userPath = varAst.Item1;
|
|
astTarget = varAst.Item2.Parent;
|
|
}
|
|
else
|
|
{
|
|
CommandAst commandParameterAst = varAst.Item2 as CommandAst;
|
|
if (commandParameterAst != null)
|
|
{
|
|
userPath = varAst.Item1;
|
|
astTarget = varAst.Item2;
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(userPath))
|
|
{
|
|
Diagnostics.Assert(false, "Found a variable source but it was an unknown AST type.");
|
|
}
|
|
|
|
if (wildcardPattern.IsMatch(userPath))
|
|
{
|
|
var completedName = (userPath.IndexOfAny(s_charactersRequiringQuotes) == -1)
|
|
? prefix + userPath
|
|
: prefix + "{" + userPath + "}";
|
|
var tooltip = userPath;
|
|
var ast = astTarget;
|
|
|
|
while (ast != null)
|
|
{
|
|
var parameterAst = ast as ParameterAst;
|
|
if (parameterAst != null)
|
|
{
|
|
var typeConstraint = parameterAst.Attributes.OfType<TypeConstraintAst>().FirstOrDefault();
|
|
if (typeConstraint != null)
|
|
{
|
|
tooltip = StringUtil.Format("{0}${1}", typeConstraint.Extent.Text, userPath);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
var assignmentAst = ast.Parent as AssignmentStatementAst;
|
|
if (assignmentAst != null)
|
|
{
|
|
if (assignmentAst.Left == ast)
|
|
{
|
|
tooltip = ast.Extent.Text;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
var commandAst = ast as CommandAst;
|
|
if (commandAst != null)
|
|
{
|
|
PSTypeName discoveredType = AstTypeInference.InferTypeOf(ast, context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval).FirstOrDefault<PSTypeName>();
|
|
if (discoveredType != null)
|
|
{
|
|
tooltip = StringUtil.Format("[{0}]${1}", discoveredType.Name, userPath);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
ast = ast.Parent;
|
|
}
|
|
|
|
AddUniqueVariable(hashedResults, results, completedName, userPath, tooltip);
|
|
}
|
|
}
|
|
}
|
|
|
|
string pattern;
|
|
string provider;
|
|
if (colon == -1)
|
|
{
|
|
pattern = "variable:" + wordToComplete + "*";
|
|
provider = string.Empty;
|
|
}
|
|
else
|
|
{
|
|
provider = wordToComplete.Substring(0, colon + 1);
|
|
if (s_variableScopes.Contains(provider, StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
pattern = string.Concat("variable:", wordToComplete.AsSpan(colon + 1), "*");
|
|
}
|
|
else
|
|
{
|
|
pattern = wordToComplete + "*";
|
|
}
|
|
}
|
|
|
|
var powerShellExecutionHelper = context.Helper;
|
|
powerShellExecutionHelper
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Item").AddParameter("Path", pattern)
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object").AddParameter("Property", "Name");
|
|
|
|
Exception exceptionThrown;
|
|
var psobjs = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
if (psobjs != null)
|
|
{
|
|
foreach (dynamic psobj in psobjs)
|
|
{
|
|
var name = psobj.Name as string;
|
|
if (!string.IsNullOrEmpty(name))
|
|
{
|
|
var tooltip = name;
|
|
var variable = PSObject.Base(psobj) as PSVariable;
|
|
if (variable != null)
|
|
{
|
|
var value = variable.Value;
|
|
if (value != null)
|
|
{
|
|
tooltip = StringUtil.Format("[{0}]${1}",
|
|
ToStringCodeMethods.Type(value.GetType(),
|
|
dropNamespaces: true), name);
|
|
}
|
|
}
|
|
|
|
var completedName = (name.IndexOfAny(s_charactersRequiringQuotes) == -1)
|
|
? prefix + provider + name
|
|
: prefix + "{" + provider + name + "}";
|
|
AddUniqueVariable(hashedResults, results, completedName, name, tooltip);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (colon == -1 && "env".StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
powerShellExecutionHelper
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-Item").AddParameter("Path", "env:*")
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object").AddParameter("Property", "Key");
|
|
|
|
psobjs = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
if (psobjs != null)
|
|
{
|
|
foreach (dynamic psobj in psobjs)
|
|
{
|
|
var name = psobj.Name as string;
|
|
if (!string.IsNullOrEmpty(name))
|
|
{
|
|
name = "env:" + name;
|
|
var completedName = (name.IndexOfAny(s_charactersRequiringQuotes) == -1)
|
|
? prefix + name
|
|
: prefix + "{" + name + "}";
|
|
AddUniqueVariable(hashedResults, results, completedName, name, "[string]" + name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return variables already in session state first, because we can sometimes give better information,
|
|
// like the variables type.
|
|
foreach (var specialVariable in s_specialVariablesCache.Value)
|
|
{
|
|
if (wildcardPattern.IsMatch(specialVariable))
|
|
{
|
|
var completedName = (specialVariable.IndexOfAny(s_charactersRequiringQuotes) == -1)
|
|
? prefix + specialVariable
|
|
: prefix + "{" + specialVariable + "}";
|
|
|
|
AddUniqueVariable(hashedResults, results, completedName, specialVariable, specialVariable);
|
|
}
|
|
}
|
|
|
|
if (colon == -1)
|
|
{
|
|
// If no drive was specified, then look for matching drives/scopes
|
|
pattern = wordToComplete + "*";
|
|
powerShellExecutionHelper
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Management\\Get-PSDrive").AddParameter("Name", pattern)
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object").AddParameter("Property", "Name");
|
|
psobjs = powerShellExecutionHelper.ExecuteCurrentPowerShell(out exceptionThrown);
|
|
if (psobjs != null)
|
|
{
|
|
foreach (var psobj in psobjs)
|
|
{
|
|
var driveInfo = PSObject.Base(psobj) as PSDriveInfo;
|
|
if (driveInfo != null)
|
|
{
|
|
var name = driveInfo.Name;
|
|
if (name != null && !string.IsNullOrWhiteSpace(name) && name.Length > 1)
|
|
{
|
|
var completedName = (name.IndexOfAny(s_charactersRequiringQuotes) == -1)
|
|
? prefix + name + ":"
|
|
: prefix + "{" + name + ":}";
|
|
|
|
var tooltip = string.IsNullOrEmpty(driveInfo.Description) ? name : driveInfo.Description;
|
|
AddUniqueVariable(hashedResults, results, completedName, name, tooltip);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var scopePattern = WildcardPattern.Get(pattern, WildcardOptions.IgnoreCase);
|
|
foreach (var scope in s_variableScopes)
|
|
{
|
|
if (scopePattern.IsMatch(scope))
|
|
{
|
|
var completedName = (scope.IndexOfAny(s_charactersRequiringQuotes) == -1)
|
|
? prefix + scope
|
|
: prefix + "{" + scope + "}";
|
|
AddUniqueVariable(hashedResults, results, completedName, scope, scope);
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static void AddUniqueVariable(HashSet<string> hashedResults, List<CompletionResult> results, string completionText, string listItemText, string tooltip)
|
|
{
|
|
if (!hashedResults.Contains(completionText))
|
|
{
|
|
hashedResults.Add(completionText);
|
|
results.Add(new CompletionResult(completionText, listItemText, CompletionResultType.Variable, tooltip));
|
|
}
|
|
}
|
|
|
|
private sealed class FindVariablesVisitor : AstVisitor
|
|
{
|
|
internal Ast Top;
|
|
internal Ast CompletionVariableAst;
|
|
internal readonly List<Tuple<string, Ast>> VariableSources = new List<Tuple<string, Ast>>();
|
|
|
|
public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst)
|
|
{
|
|
if (variableExpressionAst != CompletionVariableAst)
|
|
{
|
|
VariableSources.Add(new Tuple<string, Ast>(variableExpressionAst.VariablePath.UserPath, variableExpressionAst));
|
|
}
|
|
|
|
return AstVisitAction.Continue;
|
|
}
|
|
|
|
public override AstVisitAction VisitCommand(CommandAst commandAst)
|
|
{
|
|
// MSFT: 784739 Stack overflow during tab completion of pipeline variable
|
|
// $null | % -pv p { $p<TAB> -> In this case $p is pipelinevariable
|
|
// and is used in the same command. PipelineVariables are not available
|
|
// in the command they are assigned in. Hence the following code ignores
|
|
// if the variable being completed is in the command extent.
|
|
if ((commandAst != CompletionVariableAst) && (!CompletionVariableAst.Extent.IsWithin(commandAst.Extent)))
|
|
{
|
|
string[] desiredParameters = new string[] { "PV", "PipelineVariable", "OV", "OutVariable" };
|
|
|
|
StaticBindingResult bindingResult = StaticParameterBinder.BindCommand(commandAst, false, desiredParameters);
|
|
if (bindingResult != null)
|
|
{
|
|
ParameterBindingResult parameterBindingResult;
|
|
|
|
foreach (string commandVariableParameter in desiredParameters)
|
|
{
|
|
if (bindingResult.BoundParameters.TryGetValue(commandVariableParameter, out parameterBindingResult))
|
|
{
|
|
VariableSources.Add(new Tuple<string, Ast>((string)parameterBindingResult.ConstantValue, commandAst));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return AstVisitAction.Continue;
|
|
}
|
|
|
|
public override AstVisitAction VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst)
|
|
{
|
|
return functionDefinitionAst != Top ? AstVisitAction.SkipChildren : AstVisitAction.Continue;
|
|
}
|
|
|
|
public override AstVisitAction VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst)
|
|
{
|
|
return scriptBlockExpressionAst != Top ? AstVisitAction.SkipChildren : AstVisitAction.Continue;
|
|
}
|
|
|
|
public override AstVisitAction VisitScriptBlock(ScriptBlockAst scriptBlockAst)
|
|
{
|
|
return scriptBlockAst != Top ? AstVisitAction.SkipChildren : AstVisitAction.Continue;
|
|
}
|
|
}
|
|
|
|
private static readonly Lazy<SortedSet<string>> s_specialVariablesCache = new Lazy<SortedSet<string>>(BuildSpecialVariablesCache);
|
|
|
|
private static SortedSet<string> BuildSpecialVariablesCache()
|
|
{
|
|
var result = new SortedSet<string>();
|
|
foreach (var member in typeof(SpecialVariables).GetFields(BindingFlags.NonPublic | BindingFlags.Static))
|
|
{
|
|
if (member.FieldType.Equals(typeof(string)))
|
|
{
|
|
result.Add((string)member.GetValue(null));
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
#endregion Variables
|
|
|
|
#region Comments
|
|
|
|
internal static List<CompletionResult> CompleteComment(CompletionContext context, ref int replacementIndex, ref int replacementLength)
|
|
{
|
|
if (context.WordToComplete.StartsWith("<#", StringComparison.Ordinal))
|
|
{
|
|
return CompleteCommentHelp(context, ref replacementIndex, ref replacementLength);
|
|
}
|
|
|
|
// Complete #requires statements
|
|
if (context.WordToComplete.StartsWith("#requires ", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return CompleteRequires(context, ref replacementIndex, ref replacementLength);
|
|
}
|
|
|
|
var results = new List<CompletionResult>();
|
|
|
|
// Complete the history entries
|
|
Match matchResult = Regex.Match(context.WordToComplete, @"^#([\w\-]*)$");
|
|
if (!matchResult.Success)
|
|
{
|
|
return results;
|
|
}
|
|
|
|
string wordToComplete = matchResult.Groups[1].Value;
|
|
Collection<PSObject> psobjs;
|
|
|
|
int entryId;
|
|
if (Regex.IsMatch(wordToComplete, @"^[0-9]+$") && LanguagePrimitives.TryConvertTo(wordToComplete, out entryId))
|
|
{
|
|
context.Helper.AddCommandWithPreferenceSetting("Get-History", typeof(GetHistoryCommand)).AddParameter("Id", entryId);
|
|
psobjs = context.Helper.ExecuteCurrentPowerShell(out _);
|
|
|
|
if (psobjs != null && psobjs.Count == 1)
|
|
{
|
|
var historyInfo = PSObject.Base(psobjs[0]) as HistoryInfo;
|
|
if (historyInfo != null)
|
|
{
|
|
var commandLine = historyInfo.CommandLine;
|
|
if (!string.IsNullOrEmpty(commandLine))
|
|
{
|
|
// var tooltip = "Id: " + historyInfo.Id + "\n" +
|
|
// "ExecutionStatus: " + historyInfo.ExecutionStatus + "\n" +
|
|
// "StartExecutionTime: " + historyInfo.StartExecutionTime + "\n" +
|
|
// "EndExecutionTime: " + historyInfo.EndExecutionTime + "\n";
|
|
// Use the commandLine as the Tooltip in case the commandLine is multiple lines of scripts
|
|
results.Add(new CompletionResult(commandLine, commandLine, CompletionResultType.History, commandLine));
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
wordToComplete = "*" + wordToComplete + "*";
|
|
context.Helper.AddCommandWithPreferenceSetting("Get-History", typeof(GetHistoryCommand));
|
|
|
|
psobjs = context.Helper.ExecuteCurrentPowerShell(out _);
|
|
var pattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase);
|
|
|
|
if (psobjs != null)
|
|
{
|
|
for (int index = psobjs.Count - 1; index >= 0; index--)
|
|
{
|
|
var psobj = psobjs[index];
|
|
if (!(PSObject.Base(psobj) is HistoryInfo historyInfo)) continue;
|
|
|
|
var commandLine = historyInfo.CommandLine;
|
|
if (!string.IsNullOrEmpty(commandLine) && pattern.IsMatch(commandLine))
|
|
{
|
|
// var tooltip = "Id: " + historyInfo.Id + "\n" +
|
|
// "ExecutionStatus: " + historyInfo.ExecutionStatus + "\n" +
|
|
// "StartExecutionTime: " + historyInfo.StartExecutionTime + "\n" +
|
|
// "EndExecutionTime: " + historyInfo.EndExecutionTime + "\n";
|
|
// Use the commandLine as the Tooltip in case the commandLine is multiple lines of scripts
|
|
results.Add(new CompletionResult(commandLine, commandLine, CompletionResultType.History, commandLine));
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static List<CompletionResult> CompleteRequires(CompletionContext context, ref int replacementIndex, ref int replacementLength)
|
|
{
|
|
var results = new List<CompletionResult>();
|
|
|
|
int cursorIndex = context.CursorPosition.ColumnNumber - 1;
|
|
string lineToCursor = context.CursorPosition.Line.Substring(0, cursorIndex);
|
|
|
|
// RunAsAdministrator must be the last parameter in a Requires statement so no completion if the cursor is after the parameter.
|
|
if (lineToCursor.Contains(" -RunAsAdministrator", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return results;
|
|
}
|
|
|
|
// Regex to find parameter like " -Parameter1" or " -"
|
|
MatchCollection hashtableKeyMatches = Regex.Matches(lineToCursor, @"\s+-([A-Za-z]+|$)");
|
|
if (hashtableKeyMatches.Count == 0)
|
|
{
|
|
return results;
|
|
}
|
|
|
|
Group currentParameterMatch = hashtableKeyMatches[^1].Groups[1];
|
|
|
|
// Complete the parameter if the cursor is at a parameter
|
|
if (currentParameterMatch.Index + currentParameterMatch.Length == cursorIndex)
|
|
{
|
|
string currentParameterPrefix = currentParameterMatch.Value;
|
|
|
|
replacementIndex = context.CursorPosition.Offset - currentParameterPrefix.Length;
|
|
replacementLength = currentParameterPrefix.Length;
|
|
|
|
// Produce completions for all parameters that begin with the prefix we've found,
|
|
// but which haven't already been specified in the line we need to complete
|
|
foreach (KeyValuePair<string, string> parameter in s_requiresParameters)
|
|
{
|
|
if (parameter.Key.StartsWith(currentParameterPrefix, StringComparison.OrdinalIgnoreCase)
|
|
&& !context.CursorPosition.Line.Contains($" -{parameter.Key}", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
results.Add(new CompletionResult(parameter.Key, parameter.Key, CompletionResultType.ParameterName, parameter.Value));
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// Regex to find parameter values (any text that appears after various delimiters)
|
|
hashtableKeyMatches = Regex.Matches(lineToCursor, @"(\s+|,|;|{|\""|'|=)(\w+|$)");
|
|
string currentValue;
|
|
if (hashtableKeyMatches.Count == 0)
|
|
{
|
|
currentValue = string.Empty;
|
|
}
|
|
else
|
|
{
|
|
currentValue = hashtableKeyMatches[^1].Groups[2].Value;
|
|
}
|
|
|
|
replacementIndex = context.CursorPosition.Offset - currentValue.Length;
|
|
replacementLength = currentValue.Length;
|
|
|
|
// Complete PSEdition parameter values
|
|
if (currentParameterMatch.Value.Equals("PSEdition", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
foreach (KeyValuePair<string, string> psEditionEntry in s_requiresPSEditions)
|
|
{
|
|
if (psEditionEntry.Key.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
results.Add(new CompletionResult(psEditionEntry.Key, psEditionEntry.Key, CompletionResultType.ParameterValue, psEditionEntry.Value));
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// Complete Modules module specification values
|
|
if (currentParameterMatch.Value.Equals("Modules", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
int hashtableStart = lineToCursor.LastIndexOf("@{");
|
|
int hashtableEnd = lineToCursor.LastIndexOf('}');
|
|
|
|
bool insideHashtable = hashtableStart != -1 && (hashtableEnd == -1 || hashtableEnd < hashtableStart);
|
|
|
|
// If not inside a hashtable, try to complete a module simple name
|
|
if (!insideHashtable)
|
|
{
|
|
context.WordToComplete = currentValue;
|
|
return CompleteModuleName(context, true);
|
|
}
|
|
|
|
string hashtableString = lineToCursor.Substring(hashtableStart);
|
|
|
|
// Regex to find hashtable keys with or without quotes
|
|
hashtableKeyMatches = Regex.Matches(hashtableString, @"(@{|;)\s*(?:'|\""|\w*)\w*");
|
|
|
|
// Build the list of keys we might want to complete, based on what's already been provided
|
|
var moduleSpecKeysToComplete = new HashSet<string>(s_requiresModuleSpecKeys.Keys);
|
|
bool sawModuleNameLast = false;
|
|
foreach (Match existingHashtableKeyMatch in hashtableKeyMatches)
|
|
{
|
|
string existingHashtableKey = existingHashtableKeyMatch.Value.TrimStart(s_hashtableKeyPrefixes);
|
|
|
|
if (string.IsNullOrEmpty(existingHashtableKey))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Remove the existing key we just saw
|
|
moduleSpecKeysToComplete.Remove(existingHashtableKey);
|
|
|
|
// We need to remember later if we saw "ModuleName" as the last hashtable key, for completions
|
|
if (sawModuleNameLast = existingHashtableKey.Equals("ModuleName", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// "RequiredVersion" is mutually exclusive with "ModuleVersion" and "MaximumVersion"
|
|
if (existingHashtableKey.Equals("ModuleVersion", StringComparison.OrdinalIgnoreCase)
|
|
|| existingHashtableKey.Equals("MaximumVersion", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
moduleSpecKeysToComplete.Remove("RequiredVersion");
|
|
continue;
|
|
}
|
|
|
|
if (existingHashtableKey.Equals("RequiredVersion", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
moduleSpecKeysToComplete.Remove("ModuleVersion");
|
|
moduleSpecKeysToComplete.Remove("MaximumVersion");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
Group lastHashtableKeyPrefixGroup = hashtableKeyMatches[^1].Groups[0];
|
|
|
|
// If we're not completing a key for the hashtable, try to complete module names, but nothing else
|
|
bool completingHashtableKey = lastHashtableKeyPrefixGroup.Index + lastHashtableKeyPrefixGroup.Length == hashtableString.Length;
|
|
if (!completingHashtableKey)
|
|
{
|
|
if (sawModuleNameLast)
|
|
{
|
|
context.WordToComplete = currentValue;
|
|
return CompleteModuleName(context, true);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// Now try to complete hashtable keys
|
|
foreach (string moduleSpecKey in moduleSpecKeysToComplete)
|
|
{
|
|
if (moduleSpecKey.StartsWith(currentValue, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
results.Add(new CompletionResult(moduleSpecKey, moduleSpecKey, CompletionResultType.ParameterValue, s_requiresModuleSpecKeys[moduleSpecKey]));
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static readonly IReadOnlyDictionary<string, string> s_requiresParameters = new SortedList<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
{ "Modules", "Specifies PowerShell modules that the script requires." },
|
|
{ "PSEdition", "Specifies a PowerShell edition that the script requires." },
|
|
{ "RunAsAdministrator", "Specifies that PowerShell must be running as administrator on Windows." },
|
|
{ "Version", "Specifies the minimum version of PowerShell that the script requires." },
|
|
};
|
|
|
|
private static readonly IReadOnlyDictionary<string, string> s_requiresPSEditions = new SortedList<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
{ "Core", "Specifies that the script requires PowerShell Core to run." },
|
|
{ "Desktop", "Specifies that the script requires Windows PowerShell to run." },
|
|
};
|
|
|
|
private static readonly IReadOnlyDictionary<string, string> s_requiresModuleSpecKeys = new SortedList<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
{ "ModuleName", "Required. Specifies the module name." },
|
|
{ "GUID", "Optional. Specifies the GUID of the module." },
|
|
{ "ModuleVersion", "Specifies a minimum acceptable version of the module." },
|
|
{ "RequiredVersion", "Specifies an exact, required version of the module." },
|
|
{ "MaximumVersion", "Specifies the maximum acceptable version of the module." },
|
|
};
|
|
|
|
private static readonly char[] s_hashtableKeyPrefixes = new[]
|
|
{
|
|
'@',
|
|
'{',
|
|
';',
|
|
'"',
|
|
'\'',
|
|
' ',
|
|
};
|
|
|
|
private static List<CompletionResult> CompleteCommentHelp(CompletionContext context, ref int replacementIndex, ref int replacementLength)
|
|
{
|
|
// Finds comment keywords like ".DESCRIPTION"
|
|
MatchCollection usedKeywords = Regex.Matches(context.TokenAtCursor.Text, @"(?<=^\s*\.)\w*", RegexOptions.Multiline);
|
|
if (usedKeywords.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Last keyword at or before the cursor
|
|
Match lineKeyword = null;
|
|
for (int i = usedKeywords.Count - 1; i >= 0; i--)
|
|
{
|
|
Match keyword = usedKeywords[i];
|
|
if (context.CursorPosition.Offset >= keyword.Index + context.TokenAtCursor.Extent.StartOffset)
|
|
{
|
|
lineKeyword = keyword;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (lineKeyword is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Cursor is within or at the start/end of the keyword
|
|
if (context.CursorPosition.Offset <= lineKeyword.Index + lineKeyword.Length + context.TokenAtCursor.Extent.StartOffset)
|
|
{
|
|
replacementIndex = context.TokenAtCursor.Extent.StartOffset + lineKeyword.Index;
|
|
replacementLength = lineKeyword.Value.Length;
|
|
|
|
var validKeywords = new HashSet<String>(s_commentHelpKeywords.Keys, StringComparer.OrdinalIgnoreCase);
|
|
foreach (Match keyword in usedKeywords)
|
|
{
|
|
if (keyword == lineKeyword || s_commentHelpAllowedDuplicateKeywords.Contains(keyword.Value))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
validKeywords.Remove(keyword.Value);
|
|
}
|
|
|
|
var result = new List<CompletionResult>();
|
|
foreach (string keyword in validKeywords)
|
|
{
|
|
if (keyword.StartsWith(lineKeyword.Value, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
result.Add(new CompletionResult(keyword, keyword, CompletionResultType.Keyword, s_commentHelpKeywords[keyword]));
|
|
}
|
|
}
|
|
|
|
return result.Count > 0 ? result : null;
|
|
}
|
|
|
|
// Finds the argument for the keyword (any characters following the keyword, ignoring leading/trailing whitespace). For example "C:\New folder"
|
|
Match keywordArgument = Regex.Match(context.CursorPosition.Line, @"(?<=^\s*\.\w+\s+)\S.*(?<=\S)");
|
|
int lineStartIndex = lineKeyword.Index - context.CursorPosition.Line.IndexOf(lineKeyword.Value) + context.TokenAtCursor.Extent.StartOffset;
|
|
int argumentIndex = keywordArgument.Success ? keywordArgument.Index : context.CursorPosition.ColumnNumber - 1;
|
|
|
|
replacementIndex = lineStartIndex + argumentIndex;
|
|
replacementLength = keywordArgument.Value.Length;
|
|
|
|
if (lineKeyword.Value.Equals("PARAMETER", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return CompleteCommentParameterValue(context, keywordArgument.Value);
|
|
}
|
|
|
|
if (lineKeyword.Value.Equals("FORWARDHELPTARGETNAME", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var result = new List<CompletionResult>(CompleteCommand(keywordArgument.Value, "*", CommandTypes.All));
|
|
return result.Count > 0 ? result : null;
|
|
}
|
|
|
|
if (lineKeyword.Value.Equals("FORWARDHELPCATEGORY", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var result = new List<CompletionResult>();
|
|
foreach (string category in s_commentHelpForwardCategories)
|
|
{
|
|
if (category.StartsWith(keywordArgument.Value, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
result.Add(new CompletionResult(category));
|
|
}
|
|
}
|
|
return result.Count > 0 ? result : null;
|
|
}
|
|
|
|
if (lineKeyword.Value.Equals("REMOTEHELPRUNSPACE", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var result = new List<CompletionResult>();
|
|
foreach (CompletionResult variable in CompleteVariable(keywordArgument.Value))
|
|
{
|
|
// ListItemText is used because it excludes the "$" as expected by REMOTEHELPRUNSPACE.
|
|
result.Add(new CompletionResult(variable.ListItemText, variable.ListItemText, variable.ResultType, variable.ToolTip));
|
|
}
|
|
return result.Count > 0 ? result : null;
|
|
}
|
|
|
|
if (lineKeyword.Value.Equals("EXTERNALHELP", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
context.WordToComplete = keywordArgument.Value;
|
|
var result = new List<CompletionResult>(CompleteFilename(context, containerOnly: false, (new HashSet<string>() { ".xml" })));
|
|
return result.Count > 0 ? result : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static readonly IReadOnlyDictionary<string, string> s_commentHelpKeywords = new SortedList<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
{ "SYNOPSIS", "A brief description of the function or script. This keyword can be used only once in each topic." },
|
|
{ "DESCRIPTION", "A detailed description of the function or script. This keyword can be used only once in each topic." },
|
|
{ "PARAMETER", ".PARAMETER <Parameter-Name>\nThe description of a parameter. Add a .PARAMETER keyword for each parameter in the function or script syntax." },
|
|
{ "EXAMPLE", "A sample command that uses the function or script, optionally followed by sample output and a description. Repeat this keyword for each example." },
|
|
{ "INPUTS", "The .NET types of objects that can be piped to the function or script. You can also include a description of the input objects." },
|
|
{ "OUTPUTS", "The .NET type of the objects that the cmdlet returns. You can also include a description of the returned objects." },
|
|
{ "NOTES", "Additional information about the function or script." },
|
|
{ "LINK", "The name of a related topic. Repeat the .LINK keyword for each related topic. The .Link keyword content can also include a URI to an online version of the same help topic." },
|
|
{ "COMPONENT", "The name of the technology or feature that the function or script uses, or to which it is related." },
|
|
{ "ROLE", "The name of the user role for the help topic." },
|
|
{ "FUNCTIONALITY", "The keywords that describe the intended use of the function." },
|
|
{ "FORWARDHELPTARGETNAME", ".FORWARDHELPTARGETNAME <Command-Name>\nRedirects to the help topic for the specified command." },
|
|
{ "FORWARDHELPCATEGORY", ".FORWARDHELPCATEGORY <Category>\nSpecifies the help category of the item in .ForwardHelpTargetName" },
|
|
{ "REMOTEHELPRUNSPACE", ".REMOTEHELPRUNSPACE <PSSession-variable>\nSpecifies a session that contains the help topic. Enter a variable that contains a PSSession object." },
|
|
{ "EXTERNALHELP", ".EXTERNALHELP <XML Help File>\nThe .ExternalHelp keyword is required when a function or script is documented in XML files." }
|
|
};
|
|
|
|
private static readonly HashSet<string> s_commentHelpAllowedDuplicateKeywords = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"PARAMETER",
|
|
"EXAMPLE",
|
|
"LINK"
|
|
};
|
|
|
|
private static readonly string[] s_commentHelpForwardCategories = new string[]
|
|
{
|
|
"Alias",
|
|
"Cmdlet",
|
|
"HelpFile",
|
|
"Function",
|
|
"Provider",
|
|
"General",
|
|
"FAQ",
|
|
"Glossary",
|
|
"ScriptCommand",
|
|
"ExternalScript",
|
|
"Filter",
|
|
"All"
|
|
};
|
|
|
|
private static FunctionDefinitionAst GetCommentHelpFunctionTarget(CompletionContext context)
|
|
{
|
|
if (context.TokenAtCursor.Kind != TokenKind.Comment)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Ast lastAst = context.RelatedAsts[^1];
|
|
Ast firstAstAfterComment = lastAst.Find(ast => ast.Extent.StartOffset >= context.TokenAtCursor.Extent.EndOffset, searchNestedScriptBlocks: false);
|
|
|
|
// Comment-based help can apply to a following function definition if it starts within 2 lines
|
|
int commentEndLine = context.TokenAtCursor.Extent.EndLineNumber + 2;
|
|
|
|
if (lastAst is NamedBlockAst)
|
|
{
|
|
// Helpblock before function inside advanced function
|
|
if (firstAstAfterComment is not null
|
|
&& firstAstAfterComment.Extent.StartLineNumber <= commentEndLine
|
|
&& firstAstAfterComment is FunctionDefinitionAst outerHelpFunctionDefAst)
|
|
{
|
|
return outerHelpFunctionDefAst;
|
|
}
|
|
|
|
// Helpblock inside function
|
|
if (lastAst.Parent.Parent is FunctionDefinitionAst innerHelpFunctionDefAst)
|
|
{
|
|
return innerHelpFunctionDefAst;
|
|
}
|
|
}
|
|
|
|
if (lastAst is ScriptBlockAst)
|
|
{
|
|
// Helpblock before function
|
|
if (firstAstAfterComment is not null
|
|
&& firstAstAfterComment.Extent.StartLineNumber <= commentEndLine
|
|
&& firstAstAfterComment is NamedBlockAst block
|
|
&& block.Statements.Count > 0
|
|
&& block.Statements[0] is FunctionDefinitionAst statement)
|
|
{
|
|
return statement;
|
|
}
|
|
|
|
// Advanced function with help inside
|
|
if (lastAst.Parent is FunctionDefinitionAst advFuncDefAst)
|
|
{
|
|
return advFuncDefAst;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static List<CompletionResult> CompleteCommentParameterValue(CompletionContext context, string wordToComplete)
|
|
{
|
|
FunctionDefinitionAst foundFunction = GetCommentHelpFunctionTarget(context);
|
|
|
|
ReadOnlyCollection<ParameterAst> foundParameters = null;
|
|
if (foundFunction is not null)
|
|
{
|
|
foundParameters = foundFunction.Parameters ?? foundFunction.Body.ParamBlock?.Parameters;
|
|
}
|
|
else if (context.RelatedAsts[^1] is ScriptBlockAst scriptAst)
|
|
{
|
|
// The helpblock is for a script file
|
|
foundParameters = scriptAst.ParamBlock?.Parameters;
|
|
}
|
|
|
|
if (foundParameters is null || foundParameters.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var parametersToShow = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (ParameterAst parameter in foundParameters)
|
|
{
|
|
if (parameter.Name.VariablePath.UserPath.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
parametersToShow.Add(parameter.Name.VariablePath.UserPath);
|
|
}
|
|
}
|
|
|
|
MatchCollection usedParameters = Regex.Matches(context.TokenAtCursor.Text, @"(?<=^\s*\.parameter\s+)\w.*(?<=\S)", RegexOptions.Multiline | RegexOptions.IgnoreCase);
|
|
foreach (Match parameter in usedParameters)
|
|
{
|
|
if (wordToComplete.Equals(parameter.Value, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
parametersToShow.Remove(parameter.Value);
|
|
}
|
|
|
|
var result = new List<CompletionResult>();
|
|
foreach (string parameter in parametersToShow)
|
|
{
|
|
result.Add(new CompletionResult(parameter));
|
|
}
|
|
|
|
return result.Count > 0 ? result : null;
|
|
}
|
|
|
|
#endregion Comments
|
|
|
|
#region Members
|
|
|
|
// List of extension methods <MethodName, Signature>
|
|
private static readonly List<Tuple<string, string>> s_extensionMethods =
|
|
new List<Tuple<string, string>>
|
|
{
|
|
new Tuple<string, string>("Where", "Where({ expression } [, mode [, numberToReturn]])"),
|
|
new Tuple<string, string>("ForEach", "ForEach(expression [, arguments...])")
|
|
};
|
|
|
|
// List of DSC collection-value variables
|
|
private static readonly HashSet<string> s_dscCollectionVariables =
|
|
new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "SelectedNodes", "AllNodes" };
|
|
|
|
internal static List<CompletionResult> CompleteMember(CompletionContext context, bool @static)
|
|
{
|
|
// If we get here, we know that either:
|
|
// * the cursor appeared immediately after a member access token ('.' or '::').
|
|
// * the parent of the ast on the cursor was a member expression.
|
|
//
|
|
// In the first case, we have 2 possibilities:
|
|
// * the last ast is an error ast because no member name was entered and we were in expression context
|
|
// * the last ast is a string constant, with something like: echo $foo.
|
|
|
|
var results = new List<CompletionResult>();
|
|
|
|
var lastAst = context.RelatedAsts.Last();
|
|
var lastAstAsMemberExpr = lastAst as MemberExpressionAst;
|
|
Ast memberNameCandidateAst = null;
|
|
ExpressionAst targetExpr = null;
|
|
if (lastAstAsMemberExpr != null)
|
|
{
|
|
// If the cursor is not inside the member name in the member expression, assume
|
|
// that the user had incomplete input, but the parser got lucky and succeeded parsing anyway.
|
|
if (context.TokenAtCursor.Extent.StartOffset >= lastAstAsMemberExpr.Member.Extent.StartOffset)
|
|
{
|
|
memberNameCandidateAst = lastAstAsMemberExpr.Member;
|
|
}
|
|
|
|
targetExpr = lastAstAsMemberExpr.Expression;
|
|
}
|
|
else
|
|
{
|
|
memberNameCandidateAst = lastAst;
|
|
}
|
|
|
|
var memberNameAst = memberNameCandidateAst as StringConstantExpressionAst;
|
|
|
|
var memberName = "*";
|
|
if (memberNameAst != null)
|
|
{
|
|
// Make sure to correctly handle: echo $foo.
|
|
if (!memberNameAst.Value.Equals(".", StringComparison.OrdinalIgnoreCase) && !memberNameAst.Value.Equals("::", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
memberName = memberNameAst.Value + "*";
|
|
}
|
|
}
|
|
else if (lastAst is not ErrorExpressionAst && targetExpr == null)
|
|
{
|
|
// I don't think we can complete anything interesting
|
|
return results;
|
|
}
|
|
|
|
var commandAst = lastAst.Parent as CommandAst;
|
|
if (commandAst != null)
|
|
{
|
|
int i;
|
|
for (i = commandAst.CommandElements.Count - 1; i >= 0; --i)
|
|
{
|
|
if (commandAst.CommandElements[i] == lastAst)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
var nextToLastAst = commandAst.CommandElements[i - 1];
|
|
var nextToLastExtent = nextToLastAst.Extent;
|
|
var lastExtent = lastAst.Extent;
|
|
if (nextToLastExtent.EndLineNumber == lastExtent.StartLineNumber &&
|
|
nextToLastExtent.EndColumnNumber == lastExtent.StartColumnNumber)
|
|
{
|
|
targetExpr = nextToLastAst as ExpressionAst;
|
|
}
|
|
}
|
|
else if (lastAst.Parent is MemberExpressionAst)
|
|
{
|
|
// If 'targetExpr' has already been set, we should skip this step. This is for some member completion
|
|
// cases in ISE. In ISE, we may add a new statement in the middle of existing statements as follows:
|
|
// $xml = New-Object Xml
|
|
// $xml.
|
|
// $xml.Save("C:\data.xml")
|
|
// In this example, we add $xml. between two existing statements, and the 'lastAst' in this case is
|
|
// a MemberExpressionAst '$xml.$xml', whose parent is still a MemberExpressionAst '$xml.$xml.Save'.
|
|
// But here we DO NOT want to re-assign 'targetExpr' to be '$xml.$xml'. 'targetExpr' in this case
|
|
// should be '$xml'.
|
|
if (targetExpr == null)
|
|
{
|
|
var memberExprAst = (MemberExpressionAst)lastAst.Parent;
|
|
targetExpr = memberExprAst.Expression;
|
|
}
|
|
}
|
|
else if (lastAst.Parent is BinaryExpressionAst && context.TokenAtCursor.Kind.Equals(TokenKind.Multiply))
|
|
{
|
|
var memberExprAst = ((BinaryExpressionAst)lastAst.Parent).Left as MemberExpressionAst;
|
|
if (memberExprAst != null)
|
|
{
|
|
targetExpr = memberExprAst.Expression;
|
|
if (memberExprAst.Member is StringConstantExpressionAst)
|
|
{
|
|
memberName = ((StringConstantExpressionAst)memberExprAst.Member).Value + "*";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (targetExpr == null)
|
|
{
|
|
// Not sure what we have, but we're not looking for members.
|
|
return results;
|
|
}
|
|
|
|
if (IsSplattedVariable(targetExpr))
|
|
{
|
|
// It's splatted variable, member expansion is not useful
|
|
return results;
|
|
}
|
|
|
|
CompleteMemberHelper(@static, memberName, targetExpr, context, results);
|
|
|
|
if (results.Count == 0)
|
|
{
|
|
PSTypeName[] inferredTypes = null;
|
|
|
|
if (@static)
|
|
{
|
|
var typeExpr = targetExpr as TypeExpressionAst;
|
|
if (typeExpr != null)
|
|
{
|
|
inferredTypes = new[] { new PSTypeName(typeExpr.TypeName) };
|
|
}
|
|
}
|
|
else
|
|
{
|
|
inferredTypes = AstTypeInference.InferTypeOf(targetExpr, context.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval).ToArray();
|
|
}
|
|
|
|
if (inferredTypes != null && inferredTypes.Length > 0)
|
|
{
|
|
// Use inferred types if we have any
|
|
CompleteMemberByInferredType(context.TypeInferenceContext, inferredTypes, results, memberName, filter: null, isStatic: @static);
|
|
}
|
|
else
|
|
{
|
|
// Handle special DSC collection variables to complete the extension methods 'Where' and 'ForEach'
|
|
// e.g. Configuration foo { node $AllNodes.<tab> --> $AllNodes.Where(
|
|
var variableAst = targetExpr as VariableExpressionAst;
|
|
var memberExprAst = targetExpr as MemberExpressionAst;
|
|
bool shouldAddExtensionMethods = false;
|
|
|
|
// We complete against extension methods 'Where' and 'ForEach' for the following DSC variables
|
|
// $SelectedNodes, $AllNodes, $ConfigurationData.AllNodes
|
|
if (variableAst != null)
|
|
{
|
|
// Handle $SelectedNodes and $AllNodes
|
|
var variablePath = variableAst.VariablePath;
|
|
if (variablePath.IsVariable && s_dscCollectionVariables.Contains(variablePath.UserPath) && IsInDscContext(variableAst))
|
|
{
|
|
shouldAddExtensionMethods = true;
|
|
}
|
|
}
|
|
else if (memberExprAst != null)
|
|
{
|
|
// Handle $ConfigurationData.AllNodes
|
|
var member = memberExprAst.Member as StringConstantExpressionAst;
|
|
if (IsConfigurationDataVariable(memberExprAst.Expression) && member != null &&
|
|
string.Equals("AllNodes", member.Value, StringComparison.OrdinalIgnoreCase) &&
|
|
IsInDscContext(memberExprAst))
|
|
{
|
|
shouldAddExtensionMethods = true;
|
|
}
|
|
}
|
|
|
|
if (shouldAddExtensionMethods)
|
|
{
|
|
CompleteExtensionMethods(memberName, results);
|
|
}
|
|
}
|
|
|
|
if (results.Count == 0)
|
|
{
|
|
// Handle '$ConfigurationData' specially to complete 'AllNodes' for it
|
|
if (IsConfigurationDataVariable(targetExpr) && IsInDscContext(targetExpr))
|
|
{
|
|
var pattern = WildcardPattern.Get(memberName, WildcardOptions.IgnoreCase);
|
|
if (pattern.IsMatch("AllNodes"))
|
|
{
|
|
results.Add(new CompletionResult("AllNodes", "AllNodes", CompletionResultType.Property, "AllNodes"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Complete members against extension methods 'Where' and 'ForEach'
|
|
/// </summary>
|
|
private static void CompleteExtensionMethods(string memberName, List<CompletionResult> results)
|
|
{
|
|
var pattern = WildcardPattern.Get(memberName, WildcardOptions.IgnoreCase);
|
|
CompleteExtensionMethods(pattern, results);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Complete members against extension methods 'Where' and 'ForEach' based on the given pattern.
|
|
/// </summary>
|
|
private static void CompleteExtensionMethods(WildcardPattern pattern, List<CompletionResult> results)
|
|
{
|
|
results.AddRange(from member in s_extensionMethods
|
|
where pattern.IsMatch(member.Item1)
|
|
select
|
|
new CompletionResult(member.Item1 + "(", member.Item1,
|
|
CompletionResultType.Method, member.Item2));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify if an expression Ast is representing the $ConfigurationData variable.
|
|
/// </summary>
|
|
private static bool IsConfigurationDataVariable(ExpressionAst targetExpr)
|
|
{
|
|
var variableExpr = targetExpr as VariableExpressionAst;
|
|
if (variableExpr != null)
|
|
{
|
|
var varPath = variableExpr.VariablePath;
|
|
if (varPath.IsVariable &&
|
|
varPath.UserPath.Equals("ConfigurationData", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verify if an expression Ast is within a configuration definition.
|
|
/// </summary>
|
|
private static bool IsInDscContext(ExpressionAst expression)
|
|
{
|
|
return Ast.GetAncestorAst<ConfigurationDefinitionAst>(expression) != null;
|
|
}
|
|
|
|
private static void CompleteFormatViewByInferredType(CompletionContext context, string[] inferredTypeNames, List<CompletionResult> results, string commandName)
|
|
{
|
|
var typeInfoDB = context.TypeInferenceContext.ExecutionContext.FormatDBManager.GetTypeInfoDataBase();
|
|
|
|
if (typeInfoDB is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Type controlBodyType = commandName switch
|
|
{
|
|
"Format-Table" => typeof(TableControlBody),
|
|
"Format-List" => typeof(ListControlBody),
|
|
"Format-Wide" => typeof(WideControlBody),
|
|
"Format-Custom" => typeof(ComplexControlBody),
|
|
_ => null
|
|
};
|
|
|
|
Diagnostics.Assert(controlBodyType is not null, "This should never happen unless a new Format-* cmdlet is added");
|
|
|
|
var wordToComplete = context.WordToComplete;
|
|
var quote = HandleDoubleAndSingleQuote(ref wordToComplete);
|
|
WildcardPattern viewPattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase);
|
|
|
|
var uniqueNames = new HashSet<string>();
|
|
foreach (ViewDefinition viewDefinition in typeInfoDB.viewDefinitionsSection.viewDefinitionList)
|
|
{
|
|
if (viewDefinition?.appliesTo is not null && controlBodyType == viewDefinition.mainControl.GetType())
|
|
{
|
|
foreach (TypeOrGroupReference applyTo in viewDefinition.appliesTo.referenceList)
|
|
{
|
|
foreach (string inferredTypeName in inferredTypeNames)
|
|
{
|
|
// We use 'StartsWith()' because 'applyTo.Name' can look like "System.Diagnostics.Process#IncludeUserName".
|
|
if (applyTo.name.StartsWith(inferredTypeName, StringComparison.OrdinalIgnoreCase)
|
|
&& uniqueNames.Add(viewDefinition.name)
|
|
&& viewPattern.IsMatch(viewDefinition.name))
|
|
{
|
|
string completionText = viewDefinition.name;
|
|
// If the string is quoted or if it contains characters that need quoting, quote it in single quotes
|
|
if (quote != string.Empty || viewDefinition.name.IndexOfAny(s_charactersRequiringQuotes) != -1)
|
|
{
|
|
completionText = "'" + completionText.Replace("'", "''") + "'";
|
|
}
|
|
|
|
results.Add(new CompletionResult(completionText, viewDefinition.name, CompletionResultType.Text, viewDefinition.name));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static void CompleteMemberByInferredType(TypeInferenceContext context, IEnumerable<PSTypeName> inferredTypes, List<CompletionResult> results, string memberName, Func<object, bool> filter, bool isStatic)
|
|
{
|
|
bool extensionMethodsAdded = false;
|
|
HashSet<string> typeNameUsed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
WildcardPattern memberNamePattern = WildcardPattern.Get(memberName, WildcardOptions.IgnoreCase);
|
|
foreach (var psTypeName in inferredTypes)
|
|
{
|
|
if (typeNameUsed.Contains(psTypeName.Name))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
typeNameUsed.Add(psTypeName.Name);
|
|
var members = context.GetMembersByInferredType(psTypeName, isStatic, filter);
|
|
foreach (var member in members)
|
|
{
|
|
AddInferredMember(member, memberNamePattern, results);
|
|
}
|
|
|
|
// Check if we need to complete against the extension methods 'Where' and 'ForEach'
|
|
if (!extensionMethodsAdded && psTypeName.Type != null && IsStaticTypeEnumerable(psTypeName.Type))
|
|
{
|
|
// Complete extension methods 'Where' and 'ForEach' for Enumerable types
|
|
extensionMethodsAdded = true;
|
|
CompleteExtensionMethods(memberNamePattern, results);
|
|
}
|
|
}
|
|
|
|
if (results.Count > 0)
|
|
{
|
|
// Sort the results
|
|
var powerShellExecutionHelper = context.Helper;
|
|
powerShellExecutionHelper
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object")
|
|
.AddParameter("Property", new[] { "ResultType", "ListItemText" })
|
|
.AddParameter("Unique");
|
|
Exception unused;
|
|
var sortedResults = powerShellExecutionHelper.ExecuteCurrentPowerShell(out unused, results);
|
|
results.Clear();
|
|
results.AddRange(sortedResults.Select(static psobj => PSObject.Base(psobj) as CompletionResult));
|
|
}
|
|
}
|
|
|
|
private static void AddInferredMember(object member, WildcardPattern memberNamePattern, List<CompletionResult> results)
|
|
{
|
|
string memberName = null;
|
|
bool isMethod = false;
|
|
Func<string> getToolTip = null;
|
|
var propertyInfo = member as PropertyInfo;
|
|
if (propertyInfo != null)
|
|
{
|
|
memberName = propertyInfo.Name;
|
|
getToolTip = () => ToStringCodeMethods.Type(propertyInfo.PropertyType) + " " + memberName
|
|
+ " { " + (propertyInfo.GetGetMethod() != null ? "get; " : string.Empty)
|
|
+ (propertyInfo.GetSetMethod() != null ? "set; " : string.Empty) + "}";
|
|
}
|
|
|
|
var fieldInfo = member as FieldInfo;
|
|
if (fieldInfo != null)
|
|
{
|
|
memberName = fieldInfo.Name;
|
|
getToolTip = () => ToStringCodeMethods.Type(fieldInfo.FieldType) + " " + memberName;
|
|
}
|
|
|
|
var methodCacheEntry = member as DotNetAdapter.MethodCacheEntry;
|
|
if (methodCacheEntry != null)
|
|
{
|
|
memberName = methodCacheEntry[0].method.Name;
|
|
isMethod = true;
|
|
getToolTip = () => string.Join("\n", methodCacheEntry.methodInformationStructures.Select(static m => m.methodDefinition));
|
|
}
|
|
|
|
var psMemberInfo = member as PSMemberInfo;
|
|
if (psMemberInfo != null)
|
|
{
|
|
memberName = psMemberInfo.Name;
|
|
isMethod = member is PSMethodInfo;
|
|
getToolTip = psMemberInfo.ToString;
|
|
}
|
|
|
|
var cimProperty = member as CimPropertyDeclaration;
|
|
if (cimProperty != null)
|
|
{
|
|
memberName = cimProperty.Name;
|
|
isMethod = false;
|
|
getToolTip = () => GetCimPropertyToString(cimProperty);
|
|
}
|
|
|
|
var memberAst = member as MemberAst;
|
|
if (memberAst != null)
|
|
{
|
|
memberName = memberAst is CompilerGeneratedMemberFunctionAst ? "new" : memberAst.Name;
|
|
isMethod = memberAst is FunctionMemberAst || memberAst is CompilerGeneratedMemberFunctionAst;
|
|
getToolTip = memberAst.GetTooltip;
|
|
}
|
|
|
|
if (memberName == null || !memberNamePattern.IsMatch(memberName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var completionResultType = isMethod ? CompletionResultType.Method : CompletionResultType.Property;
|
|
var completionText = isMethod ? memberName + "(" : memberName;
|
|
|
|
results.Add(new CompletionResult(completionText, memberName, completionResultType, getToolTip()));
|
|
}
|
|
|
|
private static string GetCimPropertyToString(CimPropertyDeclaration cimProperty)
|
|
{
|
|
string type;
|
|
switch (cimProperty.CimType)
|
|
{
|
|
case Microsoft.Management.Infrastructure.CimType.DateTime:
|
|
case Microsoft.Management.Infrastructure.CimType.Instance:
|
|
case Microsoft.Management.Infrastructure.CimType.Reference:
|
|
case Microsoft.Management.Infrastructure.CimType.DateTimeArray:
|
|
case Microsoft.Management.Infrastructure.CimType.InstanceArray:
|
|
case Microsoft.Management.Infrastructure.CimType.ReferenceArray:
|
|
type = "CimInstance#" + cimProperty.CimType.ToString();
|
|
break;
|
|
|
|
default:
|
|
type = ToStringCodeMethods.Type(CimConverter.GetDotNetType(cimProperty.CimType));
|
|
break;
|
|
}
|
|
|
|
bool isReadOnly = ((cimProperty.Flags & CimFlags.ReadOnly) == CimFlags.ReadOnly);
|
|
return type + " " + cimProperty.Name + " { get; " + (isReadOnly ? "}" : "set; }");
|
|
}
|
|
|
|
private static bool IsWriteablePropertyMember(object member)
|
|
{
|
|
var propertyInfo = member as PropertyInfo;
|
|
if (propertyInfo != null)
|
|
{
|
|
return propertyInfo.CanWrite;
|
|
}
|
|
|
|
var psPropertyInfo = member as PSPropertyInfo;
|
|
if (psPropertyInfo != null)
|
|
{
|
|
return psPropertyInfo.IsSettable;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
internal static bool IsPropertyMember(object member)
|
|
{
|
|
return member is PropertyInfo
|
|
|| member is FieldInfo
|
|
|| member is PSPropertyInfo
|
|
|| member is CimPropertyDeclaration
|
|
|| member is PropertyMemberAst;
|
|
}
|
|
|
|
private static bool IsMemberHidden(object member)
|
|
{
|
|
var psMemberInfo = member as PSMemberInfo;
|
|
if (psMemberInfo != null)
|
|
return psMemberInfo.IsHidden;
|
|
|
|
var memberInfo = member as MemberInfo;
|
|
if (memberInfo != null)
|
|
return memberInfo.GetCustomAttributes(typeof(HiddenAttribute), false).Length > 0;
|
|
|
|
var propertyMemberAst = member as PropertyMemberAst;
|
|
if (propertyMemberAst != null)
|
|
return propertyMemberAst.IsHidden;
|
|
|
|
var functionMemberAst = member as FunctionMemberAst;
|
|
if (functionMemberAst != null)
|
|
return functionMemberAst.IsHidden;
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsConstructor(object member)
|
|
{
|
|
var psMethod = member as PSMethod;
|
|
if (psMethod != null)
|
|
{
|
|
var methodCacheEntry = psMethod.adapterData as DotNetAdapter.MethodCacheEntry;
|
|
if (methodCacheEntry != null)
|
|
{
|
|
return methodCacheEntry.methodInformationStructures[0].method.IsConstructor;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
#endregion Members
|
|
|
|
#region Types
|
|
|
|
private abstract class TypeCompletionBase
|
|
{
|
|
internal abstract CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix);
|
|
|
|
internal abstract CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix, string namespaceToRemove);
|
|
|
|
internal static string RemoveBackTick(string typeName)
|
|
{
|
|
var backtick = typeName.LastIndexOf('`');
|
|
return backtick == -1 ? typeName : typeName.Substring(0, backtick);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// In OneCore PS, there is no way to retrieve all loaded assemblies. But we have the type catalog dictionary
|
|
/// which contains the full type names of all available CoreCLR .NET types. We can extract the necessary info
|
|
/// from the full type names to make type name auto-completion work.
|
|
/// This type represents a non-generic type for type name completion. It only contains information that can be
|
|
/// inferred from the full type name.
|
|
/// </summary>
|
|
private class TypeCompletionInStringFormat : TypeCompletionBase
|
|
{
|
|
/// <summary>
|
|
/// Get the full type name of the type represented by this instance.
|
|
/// </summary>
|
|
internal string FullTypeName;
|
|
|
|
/// <summary>
|
|
/// Get the short type name of the type represented by this instance.
|
|
/// </summary>
|
|
internal string ShortTypeName
|
|
{
|
|
get
|
|
{
|
|
if (_shortTypeName == null)
|
|
{
|
|
int lastDotIndex = FullTypeName.LastIndexOf('.');
|
|
int lastPlusIndex = FullTypeName.LastIndexOf('+');
|
|
_shortTypeName = lastPlusIndex != -1
|
|
? FullTypeName.Substring(lastPlusIndex + 1)
|
|
: FullTypeName.Substring(lastDotIndex + 1);
|
|
}
|
|
|
|
return _shortTypeName;
|
|
}
|
|
}
|
|
|
|
private string _shortTypeName;
|
|
|
|
/// <summary>
|
|
/// Get the namespace of the type represented by this instance.
|
|
/// </summary>
|
|
internal string Namespace
|
|
{
|
|
get
|
|
{
|
|
if (_namespace == null)
|
|
{
|
|
int lastDotIndex = FullTypeName.LastIndexOf('.');
|
|
_namespace = FullTypeName.Substring(0, lastDotIndex);
|
|
}
|
|
|
|
return _namespace;
|
|
}
|
|
}
|
|
|
|
private string _namespace;
|
|
|
|
/// <summary>
|
|
/// Construct the CompletionResult based on the information of this instance.
|
|
/// </summary>
|
|
internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix)
|
|
{
|
|
return GetCompletionResult(keyMatched, prefix, suffix, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Construct the CompletionResult based on the information of this instance.
|
|
/// </summary>
|
|
internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix, string namespaceToRemove)
|
|
{
|
|
string completion = string.IsNullOrEmpty(namespaceToRemove)
|
|
? FullTypeName
|
|
: FullTypeName.Substring(namespaceToRemove.Length + 1);
|
|
|
|
string listItem = ShortTypeName;
|
|
string tooltip = FullTypeName;
|
|
|
|
return new CompletionResult(prefix + completion + suffix, listItem, CompletionResultType.Type, tooltip);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// In OneCore PS, there is no way to retrieve all loaded assemblies. But we have the type catalog dictionary
|
|
/// which contains the full type names of all available CoreCLR .NET types. We can extract the necessary info
|
|
/// from the full type names to make type name auto-completion work.
|
|
/// This type represents a generic type for type name completion. It only contains information that can be
|
|
/// inferred from the full type name.
|
|
/// </summary>
|
|
private sealed class GenericTypeCompletionInStringFormat : TypeCompletionInStringFormat
|
|
{
|
|
/// <summary>
|
|
/// Get the number of generic type arguments required by the type represented by this instance.
|
|
/// </summary>
|
|
private int GenericArgumentCount
|
|
{
|
|
get
|
|
{
|
|
if (_genericArgumentCount == 0)
|
|
{
|
|
var backtick = FullTypeName.LastIndexOf('`');
|
|
var argCount = FullTypeName.Substring(backtick + 1);
|
|
_genericArgumentCount = LanguagePrimitives.ConvertTo<int>(argCount);
|
|
}
|
|
|
|
return _genericArgumentCount;
|
|
}
|
|
}
|
|
|
|
private int _genericArgumentCount = 0;
|
|
|
|
/// <summary>
|
|
/// Construct the CompletionResult based on the information of this instance.
|
|
/// </summary>
|
|
internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix)
|
|
{
|
|
return GetCompletionResult(keyMatched, prefix, suffix, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Construct the CompletionResult based on the information of this instance.
|
|
/// </summary>
|
|
internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix, string namespaceToRemove)
|
|
{
|
|
string fullNameWithoutBacktip = RemoveBackTick(FullTypeName);
|
|
string completion = string.IsNullOrEmpty(namespaceToRemove)
|
|
? fullNameWithoutBacktip
|
|
: fullNameWithoutBacktip.Substring(namespaceToRemove.Length + 1);
|
|
|
|
string typeName = RemoveBackTick(ShortTypeName);
|
|
var listItem = typeName + "<>";
|
|
|
|
var tooltip = new StringBuilder();
|
|
tooltip.Append(fullNameWithoutBacktip);
|
|
tooltip.Append('[');
|
|
|
|
for (int i = 0; i < GenericArgumentCount; i++)
|
|
{
|
|
if (i != 0) tooltip.Append(", ");
|
|
tooltip.Append(GenericArgumentCount == 1
|
|
? "T"
|
|
: string.Format(CultureInfo.InvariantCulture, "T{0}", i + 1));
|
|
}
|
|
|
|
tooltip.Append(']');
|
|
|
|
return new CompletionResult(prefix + completion + suffix, listItem, CompletionResultType.Type, tooltip.ToString());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This type represents a non-generic type for type name completion. It contains the actual type instance.
|
|
/// </summary>
|
|
private class TypeCompletion : TypeCompletionBase
|
|
{
|
|
internal Type Type;
|
|
|
|
protected string GetTooltipPrefix()
|
|
{
|
|
if (typeof(Delegate).IsAssignableFrom(Type))
|
|
return "Delegate ";
|
|
if (Type.IsInterface)
|
|
return "Interface ";
|
|
if (Type.IsClass)
|
|
return "Class ";
|
|
if (Type.IsEnum)
|
|
return "Enum ";
|
|
if (typeof(ValueType).IsAssignableFrom(Type))
|
|
return "Struct ";
|
|
|
|
return string.Empty; // what other interesting types are there?
|
|
}
|
|
|
|
internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix)
|
|
{
|
|
return GetCompletionResult(keyMatched, prefix, suffix, null);
|
|
}
|
|
|
|
internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix, string namespaceToRemove)
|
|
{
|
|
string completion = ToStringCodeMethods.Type(Type, false, keyMatched);
|
|
|
|
// If the completion included a namespace and ToStringCodeMethods.Type found
|
|
// an accelerator, then just use the type's FullName instead because the user
|
|
// probably didn't want the accelerator.
|
|
if (keyMatched.Contains('.') && !completion.Contains('.'))
|
|
{
|
|
completion = Type.FullName;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(namespaceToRemove) && completion.Equals(Type.FullName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Remove the namespace only if the completion text contains namespace
|
|
completion = completion.Substring(namespaceToRemove.Length + 1);
|
|
}
|
|
|
|
string listItem = Type.Name;
|
|
string tooltip = GetTooltipPrefix() + Type.FullName;
|
|
|
|
return new CompletionResult(prefix + completion + suffix, listItem, CompletionResultType.Type, tooltip);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This type represents a generic type for type name completion. It contains the actual type instance.
|
|
/// </summary>
|
|
private sealed class GenericTypeCompletion : TypeCompletion
|
|
{
|
|
internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix)
|
|
{
|
|
return GetCompletionResult(keyMatched, prefix, suffix, null);
|
|
}
|
|
|
|
internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix, string namespaceToRemove)
|
|
{
|
|
string fullNameWithoutBacktip = RemoveBackTick(Type.FullName);
|
|
string completion = string.IsNullOrEmpty(namespaceToRemove)
|
|
? fullNameWithoutBacktip
|
|
: fullNameWithoutBacktip.Substring(namespaceToRemove.Length + 1);
|
|
|
|
string typeName = RemoveBackTick(Type.Name);
|
|
var listItem = typeName + "<>";
|
|
|
|
var tooltip = new StringBuilder();
|
|
tooltip.Append(GetTooltipPrefix());
|
|
tooltip.Append(fullNameWithoutBacktip);
|
|
tooltip.Append('[');
|
|
var genericParameters = Type.GetGenericArguments();
|
|
for (int i = 0; i < genericParameters.Length; i++)
|
|
{
|
|
if (i != 0) tooltip.Append(", ");
|
|
tooltip.Append(genericParameters[i].Name);
|
|
}
|
|
|
|
tooltip.Append(']');
|
|
|
|
return new CompletionResult(prefix + completion + suffix, listItem, CompletionResultType.Type, tooltip.ToString());
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This type represents a namespace for namespace completion.
|
|
/// </summary>
|
|
private sealed class NamespaceCompletion : TypeCompletionBase
|
|
{
|
|
internal string Namespace;
|
|
|
|
internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix)
|
|
{
|
|
var listItemText = Namespace;
|
|
var dotIndex = listItemText.LastIndexOf('.');
|
|
if (dotIndex != -1)
|
|
{
|
|
listItemText = listItemText.Substring(dotIndex + 1);
|
|
}
|
|
|
|
return new CompletionResult(prefix + Namespace + suffix, listItemText, CompletionResultType.Namespace, "Namespace " + Namespace);
|
|
}
|
|
|
|
internal override CompletionResult GetCompletionResult(string keyMatched, string prefix, string suffix, string namespaceToRemove)
|
|
{
|
|
return GetCompletionResult(keyMatched, prefix, suffix);
|
|
}
|
|
}
|
|
|
|
private sealed class TypeCompletionMapping
|
|
{
|
|
// The Key is the string we'll be searching on. It could complete to various things.
|
|
internal string Key;
|
|
internal List<TypeCompletionBase> Completions = new List<TypeCompletionBase>();
|
|
}
|
|
|
|
private static TypeCompletionMapping[][] s_typeCache;
|
|
|
|
private static TypeCompletionMapping[][] InitializeTypeCache()
|
|
{
|
|
#region Process_TypeAccelerators
|
|
|
|
var entries = new Dictionary<string, TypeCompletionMapping>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var type in TypeAccelerators.Get)
|
|
{
|
|
TypeCompletionMapping entry;
|
|
var typeCompletionInstance = new TypeCompletion { Type = type.Value };
|
|
|
|
if (entries.TryGetValue(type.Key, out entry))
|
|
{
|
|
// Check if this accelerator type is already included in the mapping entry referenced by the same key.
|
|
Type acceleratorType = type.Value;
|
|
bool typeAlreadyIncluded = entry.Completions.Any(
|
|
item =>
|
|
{
|
|
var typeCompletion = item as TypeCompletion;
|
|
return typeCompletion != null && typeCompletion.Type == acceleratorType;
|
|
});
|
|
|
|
// If it's already included, skip it.
|
|
// This may happen when an accelerator name is the same as the short name of the type it represents,
|
|
// and aslo that type has more than one accelerator names. For example:
|
|
// "float" -> System.Single
|
|
// "single" -> System.Single
|
|
if (typeAlreadyIncluded) { continue; }
|
|
|
|
// If this accelerator type is not included in the mapping entry, add it in.
|
|
// This may happen when an accelerator name happens to be the short name of a different type (rare case).
|
|
entry.Completions.Add(typeCompletionInstance);
|
|
}
|
|
else
|
|
{
|
|
entries.Add(type.Key, new TypeCompletionMapping { Key = type.Key, Completions = { typeCompletionInstance } });
|
|
}
|
|
|
|
// If the full type name has already been included, then we know for sure that the short type name has also been included.
|
|
string fullTypeName = type.Value.FullName;
|
|
if (entries.ContainsKey(fullTypeName)) { continue; }
|
|
|
|
// Otherwise, add the mapping from full type name to the type
|
|
entries.Add(fullTypeName, new TypeCompletionMapping { Key = fullTypeName, Completions = { typeCompletionInstance } });
|
|
|
|
// If the short type name is the same as the accelerator name, then skip it to avoid duplication.
|
|
string shortTypeName = type.Value.Name;
|
|
if (type.Key.Equals(shortTypeName, StringComparison.OrdinalIgnoreCase)) { continue; }
|
|
|
|
// Otherwise, add a new mapping entry, or put the TypeCompletion instance in the existing mapping entry.
|
|
// For example, this may happen if both System.TimeoutException and System.ServiceProcess.TimeoutException
|
|
// are in the TypeAccelerator cache.
|
|
if (!entries.TryGetValue(shortTypeName, out entry))
|
|
{
|
|
entry = new TypeCompletionMapping { Key = shortTypeName };
|
|
entries.Add(shortTypeName, entry);
|
|
}
|
|
|
|
entry.Completions.Add(typeCompletionInstance);
|
|
}
|
|
|
|
#endregion Process_TypeAccelerators
|
|
|
|
#region Process_CoreCLR_TypeCatalog
|
|
|
|
// In CoreCLR, we have namespace-qualified type names of all available .NET Core types stored in TypeCatalog.
|
|
// Populate the type completion cache using the namespace-qualified type names.
|
|
foreach (string fullTypeName in ClrFacade.AvailableDotNetTypeNames)
|
|
{
|
|
var typeCompInString = new TypeCompletionInStringFormat { FullTypeName = fullTypeName };
|
|
HandleNamespace(entries, typeCompInString.Namespace);
|
|
HandleType(entries, fullTypeName, typeCompInString.ShortTypeName, null);
|
|
}
|
|
|
|
#endregion Process_CoreCLR_TypeCatalog
|
|
|
|
#region Process_LoadedAssemblies
|
|
|
|
foreach (Assembly assembly in ClrFacade.GetAssemblies())
|
|
{
|
|
// Ignore the assemblies that are already covered by the type catalog
|
|
if (ClrFacade.AvailableDotNetAssemblyNames.Contains(assembly.FullName)) { continue; }
|
|
|
|
try
|
|
{
|
|
foreach (Type type in assembly.GetTypes())
|
|
{
|
|
// Ignore non-public types
|
|
if (!TypeResolver.IsPublic(type)) { continue; }
|
|
|
|
HandleNamespace(entries, type.Namespace);
|
|
HandleType(entries, type.FullName, type.Name, type);
|
|
}
|
|
}
|
|
catch (ReflectionTypeLoadException)
|
|
{
|
|
}
|
|
}
|
|
|
|
#endregion Process_LoadedAssemblies
|
|
|
|
var grouping = entries.Values.GroupBy(static t => t.Key.Count(c => c == '.')).OrderBy(static g => g.Key).ToArray();
|
|
var localTypeCache = new TypeCompletionMapping[grouping.Last().Key + 1][];
|
|
foreach (var group in grouping)
|
|
{
|
|
localTypeCache[group.Key] = group.ToArray();
|
|
}
|
|
|
|
Interlocked.Exchange(ref s_typeCache, localTypeCache);
|
|
return localTypeCache;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle namespace when initializing the type cache.
|
|
/// </summary>
|
|
/// <param name="entryCache">The TypeCompletionMapping dictionary.</param>
|
|
/// <param name="namespace">The namespace.</param>
|
|
private static void HandleNamespace(Dictionary<string, TypeCompletionMapping> entryCache, string @namespace)
|
|
{
|
|
if (string.IsNullOrEmpty(@namespace))
|
|
{
|
|
return;
|
|
}
|
|
|
|
int dotIndex = 0;
|
|
while (dotIndex != -1)
|
|
{
|
|
dotIndex = @namespace.IndexOf('.', dotIndex + 1);
|
|
string subNamespace = dotIndex != -1
|
|
? @namespace.Substring(0, dotIndex)
|
|
: @namespace;
|
|
|
|
TypeCompletionMapping entry;
|
|
if (!entryCache.TryGetValue(subNamespace, out entry))
|
|
{
|
|
entry = new TypeCompletionMapping
|
|
{
|
|
Key = subNamespace,
|
|
Completions = { new NamespaceCompletion { Namespace = subNamespace } }
|
|
};
|
|
entryCache.Add(subNamespace, entry);
|
|
}
|
|
else if (!entry.Completions.OfType<NamespaceCompletion>().Any())
|
|
{
|
|
entry.Completions.Add(new NamespaceCompletion { Namespace = subNamespace });
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle a type when initializing the type cache.
|
|
/// </summary>
|
|
/// <param name="entryCache">The TypeCompletionMapping dictionary.</param>
|
|
/// <param name="fullTypeName">The full type name.</param>
|
|
/// <param name="shortTypeName">The short type name.</param>
|
|
/// <param name="actualType">The actual type object. It may be null if we are handling type information from the CoreCLR TypeCatalog.</param>
|
|
private static void HandleType(Dictionary<string, TypeCompletionMapping> entryCache, string fullTypeName, string shortTypeName, Type actualType)
|
|
{
|
|
if (string.IsNullOrEmpty(fullTypeName)) { return; }
|
|
|
|
TypeCompletionBase typeCompletionBase = null;
|
|
var backtick = fullTypeName.LastIndexOf('`');
|
|
var plusChar = fullTypeName.LastIndexOf('+');
|
|
|
|
bool isGenericTypeDefinition = backtick != -1;
|
|
bool isNested = plusChar != -1;
|
|
|
|
if (isGenericTypeDefinition)
|
|
{
|
|
// Nested generic types aren't useful for completion.
|
|
if (isNested) { return; }
|
|
|
|
typeCompletionBase = actualType != null
|
|
? (TypeCompletionBase)new GenericTypeCompletion { Type = actualType }
|
|
|
|
: new GenericTypeCompletionInStringFormat { FullTypeName = fullTypeName };
|
|
|
|
// Remove the backtick, we only want 1 generic in our results for types like Func or Action.
|
|
fullTypeName = fullTypeName.Substring(0, backtick);
|
|
shortTypeName = shortTypeName.Substring(0, shortTypeName.LastIndexOf('`'));
|
|
}
|
|
else
|
|
{
|
|
typeCompletionBase = actualType != null
|
|
? (TypeCompletionBase)new TypeCompletion { Type = actualType }
|
|
|
|
: new TypeCompletionInStringFormat { FullTypeName = fullTypeName };
|
|
}
|
|
|
|
// If the full type name has already been included, then we know for sure that the short type
|
|
// name and the accelerator type names (if there are any) have also been included.
|
|
TypeCompletionMapping entry;
|
|
if (!entryCache.TryGetValue(fullTypeName, out entry))
|
|
{
|
|
entry = new TypeCompletionMapping
|
|
{
|
|
Key = fullTypeName,
|
|
Completions = { typeCompletionBase }
|
|
};
|
|
entryCache.Add(fullTypeName, entry);
|
|
|
|
// Add a new mapping entry, or put the TypeCompletion instance in the existing mapping entry of the shortTypeName.
|
|
// For example, this may happen to System.ServiceProcess.TimeoutException when System.TimeoutException is already in the cache.
|
|
if (!entryCache.TryGetValue(shortTypeName, out entry))
|
|
{
|
|
entry = new TypeCompletionMapping { Key = shortTypeName };
|
|
entryCache.Add(shortTypeName, entry);
|
|
}
|
|
|
|
entry.Completions.Add(typeCompletionBase);
|
|
}
|
|
}
|
|
|
|
internal static List<CompletionResult> CompleteNamespace(CompletionContext context, string prefix = "", string suffix = "")
|
|
{
|
|
var localTypeCache = s_typeCache ?? InitializeTypeCache();
|
|
var results = new List<CompletionResult>();
|
|
var wordToComplete = context.WordToComplete;
|
|
var dots = wordToComplete.Count(static c => c == '.');
|
|
if (dots >= localTypeCache.Length || localTypeCache[dots] == null)
|
|
{
|
|
return results;
|
|
}
|
|
|
|
var pattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase);
|
|
|
|
foreach (var entry in localTypeCache[dots].Where(e => e.Completions.OfType<NamespaceCompletion>().Any() && pattern.IsMatch(e.Key)))
|
|
{
|
|
foreach (var completion in entry.Completions)
|
|
{
|
|
results.Add(completion.GetCompletionResult(entry.Key, prefix, suffix));
|
|
}
|
|
}
|
|
|
|
results.Sort(static (c1, c2) => string.Compare(c1.ListItemText, c2.ListItemText, StringComparison.OrdinalIgnoreCase));
|
|
return results;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Complete a typename.
|
|
/// </summary>
|
|
/// <param name="typeName"></param>
|
|
/// <returns></returns>
|
|
public static IEnumerable<CompletionResult> CompleteType(string typeName)
|
|
{
|
|
// When completing types, we don't care about the runspace, types are visible across the appdomain
|
|
var powershell = (Runspace.DefaultRunspace == null)
|
|
? PowerShell.Create()
|
|
: PowerShell.Create(RunspaceMode.CurrentRunspace);
|
|
|
|
var helper = new PowerShellExecutionHelper(powershell);
|
|
var executionContext = helper.CurrentPowerShell.Runspace.ExecutionContext;
|
|
return CompleteType(new CompletionContext { WordToComplete = typeName, Helper = helper, ExecutionContext = executionContext });
|
|
}
|
|
|
|
internal static List<CompletionResult> CompleteType(CompletionContext context, string prefix = "", string suffix = "")
|
|
{
|
|
var localTypeCache = s_typeCache ?? InitializeTypeCache();
|
|
|
|
var results = new List<CompletionResult>();
|
|
var completionTextSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var wordToComplete = context.WordToComplete;
|
|
var dots = wordToComplete.Count(static c => c == '.');
|
|
if (dots >= localTypeCache.Length || localTypeCache[dots] == null)
|
|
{
|
|
return results;
|
|
}
|
|
|
|
var pattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase);
|
|
|
|
foreach (var entry in localTypeCache[dots].Where(e => pattern.IsMatch(e.Key)))
|
|
{
|
|
foreach (var completion in entry.Completions)
|
|
{
|
|
string namespaceToRemove = GetNamespaceToRemove(context, completion);
|
|
var completionResult = completion.GetCompletionResult(entry.Key, prefix, suffix, namespaceToRemove);
|
|
|
|
// We might get the same completion result twice. For example, the type cache has:
|
|
// DscResource->System.Management.Automation.DscResourceAttribute (from accelerator)
|
|
// DscResourceAttribute->System.Management.Automation.DscResourceAttribute (from short type name)
|
|
// input '[DSCRes' can match both of them, but they actually resolves to the same completion text 'DscResource'.
|
|
if (!completionTextSet.Contains(completionResult.CompletionText))
|
|
{
|
|
results.Add(completionResult);
|
|
completionTextSet.Add(completionResult.CompletionText);
|
|
}
|
|
}
|
|
}
|
|
|
|
// this is a temporary fix. Only the type defined in the same script get complete. Need to use using Module when that is available.
|
|
if (context.RelatedAsts != null && context.RelatedAsts.Count > 0)
|
|
{
|
|
var scriptBlockAst = (ScriptBlockAst)context.RelatedAsts[0];
|
|
var typeAsts = scriptBlockAst.FindAll(static ast => ast is TypeDefinitionAst, false).Cast<TypeDefinitionAst>();
|
|
foreach (var typeAst in typeAsts.Where(ast => pattern.IsMatch(ast.Name)))
|
|
{
|
|
string toolTipPrefix = string.Empty;
|
|
if (typeAst.IsInterface)
|
|
toolTipPrefix = "Interface ";
|
|
else if (typeAst.IsClass)
|
|
toolTipPrefix = "Class ";
|
|
else if (typeAst.IsEnum)
|
|
toolTipPrefix = "Enum ";
|
|
|
|
results.Add(new CompletionResult(prefix + typeAst.Name + suffix, typeAst.Name, CompletionResultType.Type, toolTipPrefix + typeAst.Name));
|
|
}
|
|
}
|
|
|
|
results.Sort(static (c1, c2) => string.Compare(c1.ListItemText, c2.ListItemText, StringComparison.OrdinalIgnoreCase));
|
|
return results;
|
|
}
|
|
|
|
private static string GetNamespaceToRemove(CompletionContext context, TypeCompletionBase completion)
|
|
{
|
|
if (completion is NamespaceCompletion || context.RelatedAsts == null || context.RelatedAsts.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var typeCompletion = completion as TypeCompletion;
|
|
string typeNameSpace = typeCompletion != null
|
|
? typeCompletion.Type.Namespace
|
|
: ((TypeCompletionInStringFormat)completion).Namespace;
|
|
|
|
var scriptBlockAst = (ScriptBlockAst)context.RelatedAsts[0];
|
|
var matchingNsStates = scriptBlockAst.UsingStatements.Where(s =>
|
|
s.UsingStatementKind == UsingStatementKind.Namespace
|
|
&& typeNameSpace != null
|
|
&& typeNameSpace.StartsWith(s.Name.Value, StringComparison.OrdinalIgnoreCase));
|
|
|
|
string ns = string.Empty;
|
|
foreach (var nsState in matchingNsStates)
|
|
{
|
|
if (nsState.Name.Extent.Text.Length > ns.Length)
|
|
{
|
|
ns = nsState.Name.Extent.Text;
|
|
}
|
|
}
|
|
|
|
return ns;
|
|
}
|
|
|
|
#endregion Types
|
|
|
|
#region Help Topics
|
|
|
|
internal static List<CompletionResult> CompleteHelpTopics(CompletionContext context)
|
|
{
|
|
var results = new List<CompletionResult>();
|
|
string userHelpDir = HelpUtils.GetUserHomeHelpSearchPath();
|
|
string appHelpDir = Utils.GetApplicationBase(Utils.DefaultPowerShellShellID);
|
|
string currentCulture = CultureInfo.CurrentCulture.Name;
|
|
|
|
//search for help files for the current culture + en-US as fallback
|
|
var searchPaths = new string[]
|
|
{
|
|
Path.Combine(userHelpDir, currentCulture),
|
|
Path.Combine(appHelpDir, currentCulture),
|
|
Path.Combine(userHelpDir, "en-US"),
|
|
Path.Combine(appHelpDir, "en-US")
|
|
}.Distinct();
|
|
|
|
string wordToComplete = context.WordToComplete + "*";
|
|
try
|
|
{
|
|
var wildcardPattern = WildcardPattern.Get(wordToComplete, WildcardOptions.IgnoreCase);
|
|
|
|
foreach (var dir in searchPaths)
|
|
{
|
|
var currentDir = new DirectoryInfo(dir);
|
|
if (currentDir.Exists)
|
|
{
|
|
foreach (var file in currentDir.EnumerateFiles("about_*.help.txt"))
|
|
{
|
|
if (wildcardPattern.IsMatch(file.Name))
|
|
{
|
|
string topicName = file.Name.Substring(0, file.Name.LastIndexOf(".help.txt"));
|
|
results.Add(new CompletionResult(topicName));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
return results;
|
|
}
|
|
|
|
#endregion Help Topics
|
|
|
|
#region Statement Parameters
|
|
|
|
internal static List<CompletionResult> CompleteStatementFlags(TokenKind kind, string wordToComplete)
|
|
{
|
|
switch (kind)
|
|
{
|
|
case TokenKind.Switch:
|
|
|
|
Diagnostics.Assert(!string.IsNullOrEmpty(wordToComplete) && wordToComplete[0].IsDash(), "the word to complete should start with '-'");
|
|
wordToComplete = wordToComplete.Substring(1);
|
|
bool withColon = wordToComplete.EndsWith(':');
|
|
wordToComplete = withColon ? wordToComplete.Remove(wordToComplete.Length - 1) : wordToComplete;
|
|
|
|
string enumString = LanguagePrimitives.EnumSingleTypeConverter.EnumValues(typeof(SwitchFlags));
|
|
string separator = CultureInfo.CurrentUICulture.TextInfo.ListSeparator;
|
|
string[] enumArray = enumString.Split(separator, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
var pattern = WildcardPattern.Get(wordToComplete + "*", WildcardOptions.IgnoreCase);
|
|
var enumList = new List<string>();
|
|
var result = new List<CompletionResult>();
|
|
CompletionResult fullMatch = null;
|
|
|
|
foreach (string value in enumArray)
|
|
{
|
|
if (value.Equals(SwitchFlags.None.ToString(), StringComparison.OrdinalIgnoreCase)) { continue; }
|
|
|
|
if (wordToComplete.Equals(value, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string completionText = withColon ? "-" + value + ":" : "-" + value;
|
|
fullMatch = new CompletionResult(completionText, value, CompletionResultType.ParameterName, value);
|
|
continue;
|
|
}
|
|
|
|
if (pattern.IsMatch(value))
|
|
{
|
|
enumList.Add(value);
|
|
}
|
|
}
|
|
|
|
if (fullMatch != null)
|
|
{
|
|
result.Add(fullMatch);
|
|
}
|
|
|
|
enumList.Sort();
|
|
result.AddRange(from entry in enumList
|
|
let completionText = withColon ? "-" + entry + ":" : "-" + entry
|
|
select new CompletionResult(completionText, entry, CompletionResultType.ParameterName, entry));
|
|
|
|
return result;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
#endregion Statement Parameters
|
|
|
|
#region Hashtable Keys
|
|
|
|
/// <summary>
|
|
/// Generate auto complete results for hashtable key within a Dynamickeyword.
|
|
/// Results are generated based on properties of a DynamicKeyword matches given identifier.
|
|
/// For example, following "D" matches "DestinationPath"
|
|
///
|
|
/// Configuration
|
|
/// {
|
|
/// File
|
|
/// {
|
|
/// D^
|
|
/// }
|
|
/// }
|
|
/// </summary>
|
|
/// <param name="completionContext"></param>
|
|
/// <param name="ast"></param>
|
|
/// <param name="hashtableAst"></param>
|
|
/// <returns></returns>
|
|
internal static List<CompletionResult> CompleteHashtableKeyForDynamicKeyword(
|
|
CompletionContext completionContext,
|
|
DynamicKeywordStatementAst ast,
|
|
HashtableAst hashtableAst)
|
|
{
|
|
Diagnostics.Assert(ast.Keyword != null, "DynamicKeywordStatementAst.Keyword can never be null");
|
|
List<CompletionResult> results = null;
|
|
var dynamicKeywordProperties = ast.Keyword.Properties;
|
|
var memberPattern = completionContext.WordToComplete + "*";
|
|
|
|
//
|
|
// Capture all existing properties in hashtable
|
|
//
|
|
var propertiesName = new List<string>();
|
|
int cursorOffset = completionContext.CursorPosition.Offset;
|
|
foreach (var keyValueTuple in hashtableAst.KeyValuePairs)
|
|
{
|
|
var propName = keyValueTuple.Item1 as StringConstantExpressionAst;
|
|
// Exclude the property name at cursor
|
|
if (propName != null && propName.Extent.EndOffset != cursorOffset)
|
|
{
|
|
propertiesName.Add(propName.Value);
|
|
}
|
|
}
|
|
|
|
if (dynamicKeywordProperties.Count > 0)
|
|
{
|
|
// Excludes existing properties in the hashtable statement
|
|
var tempProperties = dynamicKeywordProperties.Where(p => !propertiesName.Contains(p.Key, StringComparer.OrdinalIgnoreCase));
|
|
if (tempProperties != null && tempProperties.Any())
|
|
{
|
|
results = new List<CompletionResult>();
|
|
// Filter by name
|
|
var wildcardPattern = WildcardPattern.Get(memberPattern, WildcardOptions.IgnoreCase);
|
|
var matchedResults = tempProperties.Where(p => wildcardPattern.IsMatch(p.Key));
|
|
if (matchedResults == null || !matchedResults.Any())
|
|
{
|
|
// Fallback to all non-exist properties in the hashtable statement
|
|
matchedResults = tempProperties;
|
|
}
|
|
|
|
foreach (var p in matchedResults)
|
|
{
|
|
string psTypeName = LanguagePrimitives.ConvertTypeNameToPSTypeName(p.Value.TypeConstraint);
|
|
if (psTypeName == "[]" || string.IsNullOrEmpty(psTypeName))
|
|
{
|
|
psTypeName = "[" + p.Value.TypeConstraint + "]";
|
|
}
|
|
|
|
if (string.Equals(psTypeName, "[MSFT_Credential]", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
psTypeName = "[pscredential]";
|
|
}
|
|
|
|
results.Add(new CompletionResult(
|
|
p.Key + " = ",
|
|
p.Key,
|
|
CompletionResultType.Property,
|
|
psTypeName));
|
|
}
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
internal static List<CompletionResult> CompleteHashtableKey(CompletionContext completionContext, HashtableAst hashtableAst)
|
|
{
|
|
var typeAst = hashtableAst.Parent as ConvertExpressionAst;
|
|
if (typeAst != null)
|
|
{
|
|
var result = new List<CompletionResult>();
|
|
CompleteMemberByInferredType(
|
|
completionContext.TypeInferenceContext, AstTypeInference.InferTypeOf(typeAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval),
|
|
result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false);
|
|
return result;
|
|
}
|
|
|
|
// hashtable arguments sometimes have expected keys. Examples:
|
|
// new-object System.Drawing.Point -prop @{ X=1; Y=1 }
|
|
// dir | sort-object -prop @{Expression=... ; Ascending=... }
|
|
// format-table -Property
|
|
// Expression
|
|
// FormatString
|
|
// Label
|
|
// Width
|
|
// Alignment
|
|
// format-list -Property
|
|
// Expression
|
|
// FormatString
|
|
// Label
|
|
// format-custom -Property
|
|
// Expression
|
|
// Depth
|
|
// format-* -GroupBy
|
|
// Expression
|
|
// FormatString
|
|
// Label
|
|
//
|
|
|
|
// Find out if we are in a command argument. Consider the following possibilities:
|
|
// cmd @{}
|
|
// cmd -foo @{}
|
|
// cmd -foo:@{}
|
|
// cmd @{},@{}
|
|
// cmd -foo @{},@{}
|
|
// cmd -foo:@{},@{}
|
|
|
|
var ast = hashtableAst.Parent;
|
|
|
|
// Handle completion for hashtable within DynamicKeyword statement
|
|
var dynamicKeywordStatementAst = ast as DynamicKeywordStatementAst;
|
|
if (dynamicKeywordStatementAst != null)
|
|
{
|
|
return CompleteHashtableKeyForDynamicKeyword(completionContext, dynamicKeywordStatementAst, hashtableAst);
|
|
}
|
|
|
|
if (ast is ArrayLiteralAst)
|
|
{
|
|
ast = ast.Parent;
|
|
}
|
|
|
|
if (ast is CommandParameterAst)
|
|
{
|
|
ast = ast.Parent;
|
|
}
|
|
|
|
var commandAst = ast as CommandAst;
|
|
if (commandAst != null)
|
|
{
|
|
var binding = new PseudoParameterBinder().DoPseudoParameterBinding(commandAst, null, null, bindingType: PseudoParameterBinder.BindingType.ArgumentCompletion);
|
|
if (binding == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string parameterName = null;
|
|
foreach (var boundArg in binding.BoundArguments)
|
|
{
|
|
var astPair = boundArg.Value as AstPair;
|
|
if (astPair != null)
|
|
{
|
|
if (astPair.Argument == hashtableAst)
|
|
{
|
|
parameterName = boundArg.Key;
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
var astArrayPair = boundArg.Value as AstArrayPair;
|
|
if (astArrayPair != null)
|
|
{
|
|
if (astArrayPair.Argument.Contains(hashtableAst))
|
|
{
|
|
parameterName = boundArg.Key;
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (parameterName != null)
|
|
{
|
|
if (parameterName.Equals("GroupBy", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
switch (binding.CommandName)
|
|
{
|
|
case "Format-Table":
|
|
case "Format-List":
|
|
case "Format-Wide":
|
|
case "Format-Custom":
|
|
return GetSpecialHashTableKeyMembers("Expression", "FormatString", "Label");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
switch (binding.CommandName)
|
|
{
|
|
case "New-Object":
|
|
var inferredType = AstTypeInference.InferTypeOf(commandAst, completionContext.TypeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval);
|
|
var result = new List<CompletionResult>();
|
|
CompleteMemberByInferredType(
|
|
completionContext.TypeInferenceContext, inferredType,
|
|
result, completionContext.WordToComplete + "*", IsWriteablePropertyMember, isStatic: false);
|
|
return result;
|
|
case "Select-Object":
|
|
return GetSpecialHashTableKeyMembers("Name", "Expression");
|
|
case "Sort-Object":
|
|
return GetSpecialHashTableKeyMembers("Expression", "Ascending", "Descending");
|
|
case "Group-Object":
|
|
return GetSpecialHashTableKeyMembers("Expression");
|
|
case "Format-Table":
|
|
return GetSpecialHashTableKeyMembers("Expression", "FormatString", "Label", "Width", "Alignment");
|
|
case "Format-List":
|
|
return GetSpecialHashTableKeyMembers("Expression", "FormatString", "Label");
|
|
case "Format-Wide":
|
|
return GetSpecialHashTableKeyMembers("Expression", "FormatString");
|
|
case "Format-Custom":
|
|
return GetSpecialHashTableKeyMembers("Expression", "Depth");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static List<CompletionResult> GetSpecialHashTableKeyMembers(params string[] keys)
|
|
{
|
|
// Resources were removed because they missed the deadline for loc.
|
|
// return keys.Select(key => new CompletionResult(key, key, CompletionResultType.Property,
|
|
// ResourceManagerCache.GetResourceString(typeof(CompletionCompleters).Assembly,
|
|
// "TabCompletionStrings", key + "HashKeyDescription"))).ToList();
|
|
return keys.Select(static key => new CompletionResult(key, key, CompletionResultType.Property, key)).ToList();
|
|
}
|
|
|
|
#endregion Hashtable Keys
|
|
|
|
#region Helpers
|
|
|
|
internal static bool IsPathSafelyExpandable(ExpandableStringExpressionAst expandableStringAst, string extraText, ExecutionContext executionContext, out string expandedString)
|
|
{
|
|
expandedString = null;
|
|
// Expand the string if its type is DoubleQuoted or BareWord
|
|
var constType = expandableStringAst.StringConstantType;
|
|
if (constType == StringConstantType.DoubleQuotedHereString) { return false; }
|
|
|
|
Diagnostics.Assert(
|
|
constType == StringConstantType.BareWord ||
|
|
(constType == StringConstantType.DoubleQuoted && expandableStringAst.Extent.Text[0].IsDoubleQuote()),
|
|
"the string to be expanded should be either BareWord or DoubleQuoted");
|
|
|
|
var varValues = new List<string>();
|
|
foreach (ExpressionAst nestedAst in expandableStringAst.NestedExpressions)
|
|
{
|
|
if (!(nestedAst is VariableExpressionAst variableAst)) { return false; }
|
|
|
|
string strValue = CombineVariableWithPartialPath(variableAst, null, executionContext);
|
|
if (strValue != null)
|
|
{
|
|
varValues.Add(strValue);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var formattedString = string.Format(CultureInfo.InvariantCulture, expandableStringAst.FormatExpression, varValues.ToArray());
|
|
string quote = (constType == StringConstantType.DoubleQuoted) ? "\"" : string.Empty;
|
|
|
|
expandedString = quote + formattedString + extraText + quote;
|
|
return true;
|
|
}
|
|
|
|
internal static string CombineVariableWithPartialPath(VariableExpressionAst variableAst, string extraText, ExecutionContext executionContext)
|
|
{
|
|
var varPath = variableAst.VariablePath;
|
|
if (varPath.IsVariable || varPath.DriveName.Equals("env", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
try
|
|
{
|
|
// We check the strict mode inside GetVariableValue
|
|
object value = VariableOps.GetVariableValue(varPath, executionContext, variableAst);
|
|
var strValue = (value == null) ? string.Empty : value as string;
|
|
|
|
if (strValue == null)
|
|
{
|
|
object baseObj = PSObject.Base(value);
|
|
if (baseObj is string || baseObj.GetType().IsPrimitive)
|
|
{
|
|
strValue = LanguagePrimitives.ConvertTo<string>(value);
|
|
}
|
|
}
|
|
|
|
if (strValue != null)
|
|
{
|
|
return strValue + extraText;
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
internal static string HandleDoubleAndSingleQuote(ref string wordToComplete)
|
|
{
|
|
string quote = string.Empty;
|
|
|
|
if (!string.IsNullOrEmpty(wordToComplete) && (wordToComplete[0].IsSingleQuote() || wordToComplete[0].IsDoubleQuote()))
|
|
{
|
|
char frontQuote = wordToComplete[0];
|
|
int length = wordToComplete.Length;
|
|
|
|
if (length == 1)
|
|
{
|
|
wordToComplete = string.Empty;
|
|
quote = frontQuote.IsSingleQuote() ? "'" : "\"";
|
|
}
|
|
else if (length > 1)
|
|
{
|
|
if ((wordToComplete[length - 1].IsDoubleQuote() && frontQuote.IsDoubleQuote()) || (wordToComplete[length - 1].IsSingleQuote() && frontQuote.IsSingleQuote()))
|
|
{
|
|
wordToComplete = wordToComplete.Substring(1, length - 2);
|
|
quote = frontQuote.IsSingleQuote() ? "'" : "\"";
|
|
}
|
|
else if (!wordToComplete[length - 1].IsDoubleQuote() && !wordToComplete[length - 1].IsSingleQuote())
|
|
{
|
|
wordToComplete = wordToComplete.Substring(1);
|
|
quote = frontQuote.IsSingleQuote() ? "'" : "\"";
|
|
}
|
|
}
|
|
}
|
|
|
|
return quote;
|
|
}
|
|
|
|
internal static bool IsSplattedVariable(Ast targetExpr)
|
|
{
|
|
if (targetExpr is VariableExpressionAst && ((VariableExpressionAst)targetExpr).Splatted)
|
|
{
|
|
// It's splatted variable, member expansion is not useful
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
internal static void CompleteMemberHelper(
|
|
bool @static,
|
|
string memberName,
|
|
ExpressionAst targetExpr,
|
|
CompletionContext context,
|
|
List<CompletionResult> results)
|
|
{
|
|
object value;
|
|
if (SafeExprEvaluator.TrySafeEval(targetExpr, context.ExecutionContext, out value) && value != null)
|
|
{
|
|
if (targetExpr is ArrayExpressionAst && value is not object[])
|
|
{
|
|
// When the array contains only one element, the evaluation result would be that element. We wrap it into an array
|
|
value = new[] { value };
|
|
}
|
|
|
|
// Instead of Get-Member, we access the members directly and send as input to the pipe.
|
|
var powerShellExecutionHelper = context.Helper;
|
|
powerShellExecutionHelper
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Core\\Where-Object")
|
|
.AddParameter("Property", "Name")
|
|
.AddParameter("Like")
|
|
.AddParameter("Value", memberName)
|
|
.AddCommandWithPreferenceSetting("Microsoft.PowerShell.Utility\\Sort-Object")
|
|
.AddParameter("Property", new object[] { "MemberType", "Name" });
|
|
|
|
IEnumerable members;
|
|
if (@static)
|
|
{
|
|
if (!(PSObject.Base(value) is Type type))
|
|
{
|
|
return;
|
|
}
|
|
|
|
members = PSObject.DotNetStaticAdapter.BaseGetMembers<PSMemberInfo>(type);
|
|
}
|
|
else
|
|
{
|
|
members = PSObject.AsPSObject(value).Members;
|
|
}
|
|
|
|
var sortedMembers = powerShellExecutionHelper.ExecuteCurrentPowerShell(out _, members);
|
|
|
|
foreach (var member in sortedMembers)
|
|
{
|
|
var memberInfo = (PSMemberInfo)PSObject.Base(member);
|
|
if (memberInfo.IsHidden)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var completionText = memberInfo.Name;
|
|
|
|
// Handle scenarios like this: $aa | add-member 'a b' 23; $aa.a<tab>
|
|
if (completionText.IndexOfAny(s_charactersRequiringQuotes) != -1)
|
|
{
|
|
completionText = completionText.Replace("'", "''");
|
|
completionText = "'" + completionText + "'";
|
|
}
|
|
|
|
var isMethod = memberInfo is PSMethodInfo;
|
|
if (isMethod)
|
|
{
|
|
var isSpecial = (memberInfo is PSMethod) && ((PSMethod)memberInfo).IsSpecial;
|
|
if (isSpecial)
|
|
continue;
|
|
completionText += '(';
|
|
}
|
|
|
|
string tooltip = memberInfo.ToString();
|
|
if (tooltip.Contains("),", StringComparison.Ordinal))
|
|
{
|
|
var overloads = tooltip.Split("),", StringSplitOptions.RemoveEmptyEntries);
|
|
var newTooltip = new StringBuilder();
|
|
foreach (var overload in overloads)
|
|
{
|
|
newTooltip.Append(overload.Trim() + ")\r\n");
|
|
}
|
|
|
|
newTooltip.Remove(newTooltip.Length - 3, 3);
|
|
tooltip = newTooltip.ToString();
|
|
}
|
|
|
|
results.Add(
|
|
new CompletionResult(completionText, memberInfo.Name,
|
|
isMethod ? CompletionResultType.Method : CompletionResultType.Property,
|
|
tooltip));
|
|
}
|
|
|
|
var dictionary = PSObject.Base(value) as IDictionary;
|
|
if (dictionary != null)
|
|
{
|
|
var pattern = WildcardPattern.Get(memberName, WildcardOptions.IgnoreCase);
|
|
foreach (DictionaryEntry entry in dictionary)
|
|
{
|
|
if (!(entry.Key is string key))
|
|
continue;
|
|
|
|
if (pattern.IsMatch(key))
|
|
{
|
|
// Handle scenarios like this: $hashtable["abc#d"] = 100; $hashtable.ab<tab>
|
|
if (key.IndexOfAny(s_charactersRequiringQuotes) != -1)
|
|
{
|
|
key = key.Replace("'", "''");
|
|
key = "'" + key + "'";
|
|
}
|
|
|
|
results.Add(new CompletionResult(key, key, CompletionResultType.Property, key));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!@static && IsValueEnumerable(PSObject.Base(value)))
|
|
{
|
|
// Complete extension methods 'Where' and 'ForEach' for Enumerable values
|
|
CompleteExtensionMethods(memberName, results);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if a value is treated as Enumerable in powershell.
|
|
/// </summary>
|
|
private static bool IsValueEnumerable(object value)
|
|
{
|
|
object baseValue = PSObject.Base(value);
|
|
|
|
if (baseValue == null || baseValue is string || baseValue is PSObject ||
|
|
baseValue is IDictionary || baseValue is System.Xml.XmlNode)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (baseValue is IEnumerable || baseValue is IEnumerator || baseValue is DataTable)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if a strong type is treated as Enumerable in powershell.
|
|
/// </summary>
|
|
private static bool IsStaticTypeEnumerable(Type type)
|
|
{
|
|
if (type.Equals(typeof(string)) || typeof(IDictionary).IsAssignableFrom(type) || typeof(System.Xml.XmlNode).IsAssignableFrom(type))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (typeof(IEnumerable).IsAssignableFrom(type) || typeof(IEnumerator).IsAssignableFrom(type))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool CompletionRequiresQuotes(string completion, bool escape)
|
|
{
|
|
// If the tokenizer sees the completion as more than two tokens, or if there is some error, then
|
|
// some form of quoting is necessary (if it's a variable, we'd need ${}, filenames would need [], etc.)
|
|
|
|
Language.Token[] tokens;
|
|
ParseError[] errors;
|
|
Language.Parser.ParseInput(completion, out tokens, out errors);
|
|
|
|
char[] charToCheck = escape ? new[] { '$', '[', ']', '`' } : new[] { '$', '`' };
|
|
|
|
// Expect no errors and 2 tokens (1 is for our completion, the other is eof)
|
|
// Or if the completion is a keyword, we ignore the errors
|
|
bool requireQuote = !(errors.Length == 0 && tokens.Length == 2);
|
|
if ((!requireQuote && tokens[0] is StringToken) ||
|
|
(tokens.Length == 2 && (tokens[0].TokenFlags & TokenFlags.Keyword) != 0))
|
|
{
|
|
requireQuote = false;
|
|
var value = tokens[0].Text;
|
|
if (value.IndexOfAny(charToCheck) != -1)
|
|
requireQuote = true;
|
|
}
|
|
|
|
return requireQuote;
|
|
}
|
|
|
|
private static bool ProviderSpecified(string path)
|
|
{
|
|
var index = path.IndexOf(':');
|
|
return index != -1 && index + 1 < path.Length && path[index + 1] == ':';
|
|
}
|
|
|
|
private static Type GetEffectiveParameterType(Type type)
|
|
{
|
|
var underlying = Nullable.GetUnderlyingType(type);
|
|
return underlying ?? type;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Turn on the "LiteralPaths" option.
|
|
/// </summary>
|
|
/// <param name="completionContext"></param>
|
|
/// <returns>
|
|
/// Indicate whether the "LiteralPaths" option needs to be removed after operation
|
|
/// </returns>
|
|
private static bool TurnOnLiteralPathOption(CompletionContext completionContext)
|
|
{
|
|
bool clearLiteralPathsKey = false;
|
|
|
|
if (completionContext.Options == null)
|
|
{
|
|
completionContext.Options = new Hashtable { { "LiteralPaths", true } };
|
|
clearLiteralPathsKey = 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;
|
|
}
|
|
|
|
return clearLiteralPathsKey;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return whether we need to add ampersand when it's necessary.
|
|
/// </summary>
|
|
/// <param name="context"></param>
|
|
/// <param name="defaultChoice"></param>
|
|
/// <returns></returns>
|
|
internal static bool IsAmpersandNeeded(CompletionContext context, bool defaultChoice)
|
|
{
|
|
if (context.RelatedAsts != null && !string.IsNullOrEmpty(context.WordToComplete))
|
|
{
|
|
var lastAst = context.RelatedAsts.Last();
|
|
var parent = lastAst.Parent as CommandAst;
|
|
|
|
if (parent != null && parent.CommandElements.Count == 1 &&
|
|
((!defaultChoice && parent.InvocationOperator == TokenKind.Unknown) ||
|
|
(defaultChoice && parent.InvocationOperator != TokenKind.Unknown)))
|
|
{
|
|
// - When the default choice is NOT to add ampersand, we only return true
|
|
// when the invocation operator is NOT specified.
|
|
// - When the default choice is to add ampersand, we only return false
|
|
// when the invocation operator is specified.
|
|
defaultChoice = !defaultChoice;
|
|
}
|
|
}
|
|
|
|
return defaultChoice;
|
|
}
|
|
|
|
private sealed class ItemPathComparer : IComparer<PSObject>
|
|
{
|
|
public int Compare(PSObject x, PSObject y)
|
|
{
|
|
var xPathInfo = PSObject.Base(x) as PathInfo;
|
|
var xFileInfo = PSObject.Base(x) as IO.FileSystemInfo;
|
|
var xPathStr = PSObject.Base(x) as string;
|
|
|
|
var yPathInfo = PSObject.Base(y) as PathInfo;
|
|
var yFileInfo = PSObject.Base(y) as IO.FileSystemInfo;
|
|
var yPathStr = PSObject.Base(y) as string;
|
|
|
|
string xPath = null, yPath = null;
|
|
|
|
if (xPathInfo != null)
|
|
xPath = xPathInfo.ProviderPath;
|
|
else if (xFileInfo != null)
|
|
xPath = xFileInfo.FullName;
|
|
else if (xPathStr != null)
|
|
xPath = xPathStr;
|
|
|
|
if (yPathInfo != null)
|
|
yPath = yPathInfo.ProviderPath;
|
|
else if (yFileInfo != null)
|
|
yPath = yFileInfo.FullName;
|
|
else if (yPathStr != null)
|
|
yPath = yPathStr;
|
|
|
|
if (string.IsNullOrEmpty(xPath) || string.IsNullOrEmpty(yPath))
|
|
Diagnostics.Assert(false, "Base object of item PSObject should be either PathInfo or FileSystemInfo");
|
|
|
|
return string.Compare(xPath, yPath, StringComparison.CurrentCultureIgnoreCase);
|
|
}
|
|
}
|
|
|
|
private sealed class CommandNameComparer : IComparer<PSObject>
|
|
{
|
|
public int Compare(PSObject x, PSObject y)
|
|
{
|
|
string xName = null;
|
|
string yName = null;
|
|
|
|
object xObj = PSObject.Base(x);
|
|
object yObj = PSObject.Base(y);
|
|
|
|
var xCommandInfo = xObj as CommandInfo;
|
|
xName = xCommandInfo != null ? xCommandInfo.Name : xObj as string;
|
|
|
|
var yCommandInfo = yObj as CommandInfo;
|
|
yName = yCommandInfo != null ? yCommandInfo.Name : yObj as string;
|
|
|
|
if (xName == null || yName == null)
|
|
Diagnostics.Assert(false, "Base object of Command PSObject should be either CommandInfo or string");
|
|
|
|
return string.Compare(xName, yName, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
|
|
#endregion Helpers
|
|
}
|
|
|
|
/// <summary>
|
|
/// This class is very similar to the restricted language checker, but it is meant to allow more things, yet still
|
|
/// be considered "safe", at least in the sense that tab completion can rely on it to not do bad things. The primary
|
|
/// use is for intellisense where you don't want to run arbitrary code, but you do want to know the values
|
|
/// of various expressions so you can get the members.
|
|
/// </summary>
|
|
internal class SafeExprEvaluator : ICustomAstVisitor2
|
|
{
|
|
internal static bool TrySafeEval(ExpressionAst ast, ExecutionContext executionContext, out object value)
|
|
{
|
|
if (!(bool)ast.Accept(new SafeExprEvaluator()))
|
|
{
|
|
value = null;
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
// ConstrainedLanguage has already been applied as necessary when we construct CompletionContext
|
|
Diagnostics.Assert(!(executionContext.HasRunspaceEverUsedConstrainedLanguageMode && executionContext.LanguageMode != PSLanguageMode.ConstrainedLanguage),
|
|
"If the runspace has ever used constrained language mode, then the current language mode should already be set to constrained language");
|
|
|
|
// We're passing 'true' here for isTrustedInput, because SafeExprEvaluator ensures that the AST
|
|
// has no dangerous side-effects such as arbitrary expression evaluation. It does require variable
|
|
// access and a few other minor things, which staples of tab completion:
|
|
//
|
|
// $t = Get-Process
|
|
// $t[0].MainModule.<TAB>
|
|
//
|
|
value = Compiler.GetExpressionValue(ast, true, executionContext);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
value = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public object VisitErrorStatement(ErrorStatementAst errorStatementAst) { return false; }
|
|
|
|
public object VisitErrorExpression(ErrorExpressionAst errorExpressionAst) { return false; }
|
|
|
|
public object VisitScriptBlock(ScriptBlockAst scriptBlockAst) { return false; }
|
|
|
|
public object VisitParamBlock(ParamBlockAst paramBlockAst) { return false; }
|
|
|
|
public object VisitNamedBlock(NamedBlockAst namedBlockAst) { return false; }
|
|
|
|
public object VisitTypeConstraint(TypeConstraintAst typeConstraintAst) { return false; }
|
|
|
|
public object VisitAttribute(AttributeAst attributeAst) { return false; }
|
|
|
|
public object VisitNamedAttributeArgument(NamedAttributeArgumentAst namedAttributeArgumentAst) { return false; }
|
|
|
|
public object VisitParameter(ParameterAst parameterAst) { return false; }
|
|
|
|
public object VisitFunctionDefinition(FunctionDefinitionAst functionDefinitionAst) { return false; }
|
|
|
|
public object VisitIfStatement(IfStatementAst ifStmtAst) { return false; }
|
|
|
|
public object VisitTrap(TrapStatementAst trapStatementAst) { return false; }
|
|
|
|
public object VisitSwitchStatement(SwitchStatementAst switchStatementAst) { return false; }
|
|
|
|
public object VisitDataStatement(DataStatementAst dataStatementAst) { return false; }
|
|
|
|
public object VisitForEachStatement(ForEachStatementAst forEachStatementAst) { return false; }
|
|
|
|
public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst) { return false; }
|
|
|
|
public object VisitForStatement(ForStatementAst forStatementAst) { return false; }
|
|
|
|
public object VisitWhileStatement(WhileStatementAst whileStatementAst) { return false; }
|
|
|
|
public object VisitCatchClause(CatchClauseAst catchClauseAst) { return false; }
|
|
|
|
public object VisitTryStatement(TryStatementAst tryStatementAst) { return false; }
|
|
|
|
public object VisitBreakStatement(BreakStatementAst breakStatementAst) { return false; }
|
|
|
|
public object VisitContinueStatement(ContinueStatementAst continueStatementAst) { return false; }
|
|
|
|
public object VisitReturnStatement(ReturnStatementAst returnStatementAst) { return false; }
|
|
|
|
public object VisitExitStatement(ExitStatementAst exitStatementAst) { return false; }
|
|
|
|
public object VisitThrowStatement(ThrowStatementAst throwStatementAst) { return false; }
|
|
|
|
public object VisitDoUntilStatement(DoUntilStatementAst doUntilStatementAst) { return false; }
|
|
|
|
public object VisitAssignmentStatement(AssignmentStatementAst assignmentStatementAst) { return false; }
|
|
|
|
// REVIEW: we could relax this to allow specific commands
|
|
public object VisitCommand(CommandAst commandAst) { return false; }
|
|
|
|
public object VisitCommandExpression(CommandExpressionAst commandExpressionAst) { return false; }
|
|
|
|
public object VisitCommandParameter(CommandParameterAst commandParameterAst) { return false; }
|
|
|
|
public object VisitFileRedirection(FileRedirectionAst fileRedirectionAst) { return false; }
|
|
|
|
public object VisitMergingRedirection(MergingRedirectionAst mergingRedirectionAst) { return false; }
|
|
|
|
public object VisitExpandableStringExpression(ExpandableStringExpressionAst expandableStringExpressionAst) { return false; }
|
|
|
|
public object VisitAttributedExpression(AttributedExpressionAst attributedExpressionAst) { return false; }
|
|
|
|
public object VisitBlockStatement(BlockStatementAst blockStatementAst) { return false; }
|
|
|
|
public object VisitInvokeMemberExpression(InvokeMemberExpressionAst invokeMemberExpressionAst) { return false; }
|
|
|
|
public object VisitUsingExpression(UsingExpressionAst usingExpressionAst) { return false; }
|
|
|
|
public object VisitTypeDefinition(TypeDefinitionAst typeDefinitionAst) { return false; }
|
|
|
|
public object VisitPropertyMember(PropertyMemberAst propertyMemberAst) { return false; }
|
|
|
|
public object VisitFunctionMember(FunctionMemberAst functionMemberAst) { return false; }
|
|
|
|
public object VisitBaseCtorInvokeMemberExpression(BaseCtorInvokeMemberExpressionAst baseCtorInvokeMemberExpressionAst) { return false; }
|
|
|
|
public object VisitUsingStatement(UsingStatementAst usingStatementAst) { return false; }
|
|
|
|
public object VisitDynamicKeywordStatement(DynamicKeywordStatementAst dynamicKeywordStatementAst) { return false; }
|
|
|
|
public object VisitPipelineChain(PipelineChainAst pipelineChainAst) { return false; }
|
|
|
|
public object VisitConfigurationDefinition(ConfigurationDefinitionAst configurationDefinitionAst)
|
|
{
|
|
return configurationDefinitionAst.Body.Accept(this);
|
|
}
|
|
|
|
public object VisitStatementBlock(StatementBlockAst statementBlockAst)
|
|
{
|
|
if (statementBlockAst.Traps != null) return false;
|
|
// REVIEW: we could relax this to allow multiple statements
|
|
if (statementBlockAst.Statements.Count > 1) return false;
|
|
var pipeline = statementBlockAst.Statements.FirstOrDefault();
|
|
return pipeline != null && (bool)pipeline.Accept(this);
|
|
}
|
|
|
|
public object VisitPipeline(PipelineAst pipelineAst)
|
|
{
|
|
var expr = pipelineAst.GetPureExpression();
|
|
return expr != null && (bool)expr.Accept(this);
|
|
}
|
|
|
|
public object VisitTernaryExpression(TernaryExpressionAst ternaryExpressionAst)
|
|
{
|
|
return (bool)ternaryExpressionAst.Condition.Accept(this) &&
|
|
(bool)ternaryExpressionAst.IfTrue.Accept(this) &&
|
|
(bool)ternaryExpressionAst.IfFalse.Accept(this);
|
|
}
|
|
|
|
public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst)
|
|
{
|
|
return (bool)binaryExpressionAst.Left.Accept(this) && (bool)binaryExpressionAst.Right.Accept(this);
|
|
}
|
|
|
|
public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst)
|
|
{
|
|
return (bool)unaryExpressionAst.Child.Accept(this);
|
|
}
|
|
|
|
public object VisitConvertExpression(ConvertExpressionAst convertExpressionAst)
|
|
{
|
|
return (bool)convertExpressionAst.Child.Accept(this);
|
|
}
|
|
|
|
public object VisitConstantExpression(ConstantExpressionAst constantExpressionAst)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public object VisitStringConstantExpression(StringConstantExpressionAst stringConstantExpressionAst)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public object VisitSubExpression(SubExpressionAst subExpressionAst)
|
|
{
|
|
return subExpressionAst.SubExpression.Accept(this);
|
|
}
|
|
|
|
public object VisitVariableExpression(VariableExpressionAst variableExpressionAst)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public object VisitTypeExpression(TypeExpressionAst typeExpressionAst)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public object VisitMemberExpression(MemberExpressionAst memberExpressionAst)
|
|
{
|
|
return (bool)memberExpressionAst.Expression.Accept(this) && (bool)memberExpressionAst.Member.Accept(this);
|
|
}
|
|
|
|
public object VisitIndexExpression(IndexExpressionAst indexExpressionAst)
|
|
{
|
|
return (bool)indexExpressionAst.Target.Accept(this) && (bool)indexExpressionAst.Index.Accept(this);
|
|
}
|
|
|
|
public object VisitArrayExpression(ArrayExpressionAst arrayExpressionAst)
|
|
{
|
|
return arrayExpressionAst.SubExpression.Accept(this);
|
|
}
|
|
|
|
public object VisitArrayLiteral(ArrayLiteralAst arrayLiteralAst)
|
|
{
|
|
return arrayLiteralAst.Elements.All(e => (bool)e.Accept(this));
|
|
}
|
|
|
|
public object VisitHashtable(HashtableAst hashtableAst)
|
|
{
|
|
foreach (var keyValuePair in hashtableAst.KeyValuePairs)
|
|
{
|
|
if (!(bool)keyValuePair.Item1.Accept(this))
|
|
return false;
|
|
if (!(bool)keyValuePair.Item2.Accept(this))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public object VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public object VisitParenExpression(ParenExpressionAst parenExpressionAst)
|
|
{
|
|
return parenExpressionAst.Pipeline.Accept(this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Completes with the property names of the InputObject.
|
|
/// </summary>
|
|
internal class PropertyNameCompleter : IArgumentCompleter
|
|
{
|
|
private readonly string _parameterNameOfInput;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PropertyNameCompleter"/> class.
|
|
/// </summary>
|
|
public PropertyNameCompleter()
|
|
{
|
|
_parameterNameOfInput = "InputObject";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PropertyNameCompleter"/> class.
|
|
/// </summary>
|
|
/// <param name="parameterNameOfInput">The name of the property of the input object for witch to complete with property names.</param>
|
|
public PropertyNameCompleter(string parameterNameOfInput)
|
|
{
|
|
_parameterNameOfInput = parameterNameOfInput;
|
|
}
|
|
|
|
IEnumerable<CompletionResult> IArgumentCompleter.CompleteArgument(
|
|
string commandName,
|
|
string parameterName,
|
|
string wordToComplete,
|
|
CommandAst commandAst,
|
|
IDictionary fakeBoundParameters)
|
|
{
|
|
if (commandAst.Parent is not PipelineAst pipelineAst)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
int i;
|
|
for (i = 0; i < pipelineAst.PipelineElements.Count; i++)
|
|
{
|
|
if (pipelineAst.PipelineElements[i] == commandAst)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
var typeInferenceContext = new TypeInferenceContext();
|
|
IEnumerable<PSTypeName> prevType;
|
|
if (i == 0)
|
|
{
|
|
var parameterAst = (CommandParameterAst)commandAst.Find(ast => ast is CommandParameterAst cpa && cpa.ParameterName == "PropertyName", false);
|
|
var pseudoBinding = new PseudoParameterBinder().DoPseudoParameterBinding(commandAst, null, parameterAst, PseudoParameterBinder.BindingType.ParameterCompletion);
|
|
if (!pseudoBinding.BoundArguments.TryGetValue(_parameterNameOfInput, out var pair) || !pair.ArgumentSpecified)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (pair is AstPair astPair && astPair.Argument != null)
|
|
{
|
|
prevType = AstTypeInference.InferTypeOf(astPair.Argument, typeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
else
|
|
{
|
|
prevType = AstTypeInference.InferTypeOf(pipelineAst.PipelineElements[i - 1], typeInferenceContext, TypeInferenceRuntimePermissions.AllowSafeEval);
|
|
}
|
|
|
|
var result = new List<CompletionResult>();
|
|
|
|
CompletionCompleters.CompleteMemberByInferredType(typeInferenceContext, prevType, result, wordToComplete + "*", filter: CompletionCompleters.IsPropertyMember, isStatic: false);
|
|
return result;
|
|
}
|
|
}
|
|
}
|