mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Don't complete duplicate command names (#21113)
This commit is contained in:
@@ -329,50 +329,72 @@ namespace System.Management.Automation
|
||||
}
|
||||
}
|
||||
|
||||
List<CompletionResult> endResults = null;
|
||||
foreach (var keyValuePair in commandTable)
|
||||
{
|
||||
var commandList = keyValuePair.Value as List<object>;
|
||||
if (commandList != null)
|
||||
if (keyValuePair.Value is List<object> commandList)
|
||||
{
|
||||
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 modulesWithCommand = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var importedModules = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var commandInfoList = new List<CommandInfo>(commandList.Count);
|
||||
for (int i = 0; i < commandList.Count; i++)
|
||||
{
|
||||
var commandInfo = commandList[0] as CommandInfo;
|
||||
if (commandInfo != null && !string.IsNullOrEmpty(commandInfo.Prefix))
|
||||
if (commandList[i] is not CommandInfo commandInfo)
|
||||
{
|
||||
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;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
commandInfoList.Add(commandInfo);
|
||||
if (commandInfo.CommandType == CommandTypes.Application)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
modulesWithCommand.Add(commandInfo.ModuleName);
|
||||
if ((commandInfo.CommandType == CommandTypes.Cmdlet && commandInfo.CommandMetadata.CommandType is not null)
|
||||
|| (commandInfo.CommandType is CommandTypes.Function or CommandTypes.Filter && commandInfo.Definition != string.Empty)
|
||||
|| (commandInfo.CommandType == CommandTypes.Alias && commandInfo.Definition is not null))
|
||||
{
|
||||
// Checks if the command or source module has been imported.
|
||||
_ = importedModules.Add(commandInfo.ModuleName);
|
||||
}
|
||||
}
|
||||
|
||||
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++)
|
||||
if (commandInfoList.Count == 0)
|
||||
{
|
||||
var commandInfo = commandList[index] as CommandInfo;
|
||||
Diagnostics.Assert(commandInfo != null, "Elements should always be CommandInfo");
|
||||
continue;
|
||||
}
|
||||
|
||||
int moduleCount = modulesWithCommand.Count;
|
||||
modulesWithCommand.Clear();
|
||||
int index;
|
||||
if (commandInfoList[0].CommandType == CommandTypes.Application
|
||||
|| importedModules.Count == 1
|
||||
|| moduleCount < 2)
|
||||
{
|
||||
// We can use the short name for this command because there's no ambiguity about which command it resolves to.
|
||||
// If the first element is an application then we know there's no conflicting commands/aliases (because of the command precedence).
|
||||
// If there's just 1 module imported then the short name refers to that module (and it will be the first element in the list)
|
||||
// If there's less than 2 unique modules exporting that command then we can use the short name because it can only refer to that module.
|
||||
index = 1;
|
||||
results.Add(GetCommandNameCompletionResult(keyValuePair.Key, commandInfoList[0], addAmpersandIfNecessary, quote));
|
||||
modulesWithCommand.Add(commandInfoList[0].ModuleName);
|
||||
}
|
||||
else
|
||||
{
|
||||
index = 0;
|
||||
}
|
||||
|
||||
for (; index < commandInfoList.Count; index++)
|
||||
{
|
||||
CommandInfo commandInfo = commandInfoList[index];
|
||||
if (commandInfo.CommandType == CommandTypes.Application)
|
||||
{
|
||||
endResults.Add(GetCommandNameCompletionResult(commandInfo.Definition, commandInfo, addAmpersandIfNecessary, quote));
|
||||
results.Add(GetCommandNameCompletionResult(commandInfo.Definition, commandInfo, addAmpersandIfNecessary, quote));
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(commandInfo.ModuleName))
|
||||
else if (!string.IsNullOrEmpty(commandInfo.ModuleName) && modulesWithCommand.Add(commandInfo.ModuleName))
|
||||
{
|
||||
var name = commandInfo.ModuleName + "\\" + commandInfo.Name;
|
||||
endResults.Add(GetCommandNameCompletionResult(name, commandInfo, addAmpersandIfNecessary, quote));
|
||||
results.Add(GetCommandNameCompletionResult(name, commandInfo, addAmpersandIfNecessary, quote));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,11 +421,6 @@ namespace System.Management.Automation
|
||||
}
|
||||
}
|
||||
|
||||
if (endResults != null && endResults.Count > 0)
|
||||
{
|
||||
results.AddRange(endResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,37 @@ Describe "TabCompletion" -Tags CI {
|
||||
$res.CompletionMatches[0].CompletionText | Should -BeExactly 'notepad.exe'
|
||||
}
|
||||
|
||||
It 'Should not include duplicate command results' {
|
||||
$OldModulePath = $env:PSModulePath
|
||||
$tempDir = Join-Path -Path $TestDrive -ChildPath "TempPsModuleDir"
|
||||
try
|
||||
{
|
||||
$ModuleDirs = @(
|
||||
Join-Path $tempDir "TestModule1\1.0"
|
||||
Join-Path $tempDir "TestModule1\1.1"
|
||||
Join-Path $tempDir "TestModule2\1.0"
|
||||
)
|
||||
foreach ($Dir in $ModuleDirs)
|
||||
{
|
||||
$NewDir = New-Item -Path $Dir -ItemType Directory -Force
|
||||
$ModuleName = $NewDir.Parent.Name
|
||||
Set-Content -Value 'MyTestFunction{}' -LiteralPath "$($NewDir.FullName)\$ModuleName.psm1"
|
||||
New-ModuleManifest -Path "$($NewDir.FullName)\$ModuleName.psd1" -RootModule "$ModuleName.psm1" -FunctionsToExport "MyTestFunction" -ModuleVersion $NewDir.Name
|
||||
}
|
||||
|
||||
$env:PSModulePath += [System.IO.Path]::PathSeparator + $tempDir
|
||||
$Res = TabExpansion2 -inputScript MyTestFunction
|
||||
$Res.CompletionMatches.Count | Should -Be 2
|
||||
$SortedMatches = $Res.CompletionMatches.CompletionText | Sort-Object
|
||||
$SortedMatches[0] | Should -Be "TestModule1\MyTestFunction"
|
||||
$SortedMatches[1] | Should -Be "TestModule2\MyTestFunction"
|
||||
}
|
||||
finally
|
||||
{
|
||||
$env:PSModulePath = $OldModulePath
|
||||
}
|
||||
}
|
||||
|
||||
It 'Should complete dotnet method' {
|
||||
$res = TabExpansion2 -inputScript '(1).ToSt' -cursorColumn '(1).ToSt'.Length
|
||||
$res.CompletionMatches[0].CompletionText | Should -BeExactly 'ToString('
|
||||
|
||||
Reference in New Issue
Block a user