src/functions/completion.ps1
|
function Get-CompletionList { <# .SYNOPSIS Gets a flattened or hierarchical list of commands from a configuration map .PARAMETER map The configuration map to process. Can be a dictionary, array, scriptblock or string .PARAMETER flatten If true, flattens hierarchical commands into a single level. If false, maintains hierarchy with separators .PARAMETER separator The separator to use between parent and child command names when not flattened .PARAMETER groupMarker The marker to append to parent command names when flattened .PARAMETER listKey The key used to identify nested command lists .PARAMETER reservedKeys Array of reserved keys that should be skipped during processing .PARAMETER baseDir The base directory path for resolving relative include paths .OUTPUTS [System.Collections.Specialized.OrderedDictionary] containing the processed command list #> [OutputType([System.Collections.Specialized.OrderedDictionary])] param( [ValidateScript({ # the function do not suppurt strings, but ValidateScript iterates over the array, so for string[] we'll get string items here. # see: https://github.com/PowerShell/PowerShell/issues/6185 $_ -is [System.Collections.IDictionary] -or $_ -is [array] -or $_ -is [scriptblock] -or $_ -is [string] })] $map, [switch][bool]$flatten = $false, [switch][bool]$leafsOnly = $false, $separator = ".", $groupMarker = $null, $listKey = "list", $reservedKeys = $null, $maxDepth = -1, $baseDir = $null ) if ($maxDepth -eq 0) { return @{} } if (!$groupMarker) { $groupMarker = $flatten ? "*" : "" } $list = $map.$listKey ? $map.$listKey : $map $list = $list -is [scriptblock] ? (Invoke-Command -ScriptBlock $list) : $list $r = switch ($true) { { $list -is [System.Collections.IDictionary] } { $result = [ordered]@{} foreach ($kvp in $list.GetEnumerator()) { # Handle #include directives first (before reserved keys check) if ($kvp.key -eq "#include") { $includedEntries = Merge-IncludeDirectives $kvp.value $baseDir -flatten:$flatten -leafsOnly:$leafsOnly -separator $separator -reservedKeys $reservedKeys foreach ($inc in $includedEntries.GetEnumerator()) { $result[$inc.Key] = $inc.Value } continue } if ($kvp.key -in $reservedKeys -or $kvp.key -eq $listKey) { continue } $entry = $kvp.value $entryInfo = Test-IsParentEntry $entry $listKey -reservedKeys $reservedKeys if (!$entryInfo.IsParent) { $result["$($kvp.key)"] = $entry continue } # Add parent marker if (!$leafsOnly) { $result["$($kvp.key)$groupMarker"] = $entry } # Get nested entries and add them with appropriate prefixes $subEntries = Get-CompletionList $entry -listKey $listKey -flatten:$flatten -leafsOnly:$leafsOnly -separator $separator -reservedKeys $reservedKeys -maxDepth ($maxDepth - 1) -baseDir $baseDir foreach ($sub in $subEntries.GetEnumerator()) { $subKey = $flatten ? $sub.Key : "$($kvp.key)$separator$($sub.Key)" $result[$subKey] = $sub.value } } return $result } { $list -is [array] } { $result = [ordered]@{} $subEntries = $list | ForEach-Object { $r = [ordered]@{} } { $r[$_] = $_ } { $r } if ($subEntries) { foreach ($sub in $subEntries.GetEnumerator()) { if ($sub.key -in $reservedKeys -or $sub.key -eq $listKey) { continue } $result[$sub.key] = $sub.value } } return $result } { $list -is [string] } { throw "string type not supported" } default { throw "$($list.GetType().FullName) type not supported" } } return $r } function Merge-IncludeDirectives { <# .SYNOPSIS Processes #include directives and merges included map entries .PARAMETER includes Hashtable with include configuration (directory names as keys with prefix option) .PARAMETER baseDir Base directory for resolving relative paths #> param( [System.Collections.IDictionary]$includes, $baseDir, [switch][bool]$flatten = $false, [switch][bool]$leafsOnly = $false, $separator = ".", $reservedKeys = $null ) $result = [ordered]@{} if (!$baseDir) { $baseDir = $PWD.Path } foreach ($kvp in $includes.GetEnumerator()) { $dirName = $kvp.Key $includeConfig = $kvp.Value # Resolve the include directory path $includePath = Join-Path $baseDir $dirName if (!(Test-Path $includePath -PathType Container)) { Write-Warning "Include directory not found: $includePath" continue } # Look for map file in the included directory $mapFile = Join-Path $includePath ".build.map.ps1" if (!(Test-Path $mapFile)) { Write-Warning "Map file not found in include directory: $mapFile" continue } # Load the map from the included directory $includedMap = . $mapFile # Process the included map $includedEntries = Get-CompletionList $includedMap -flatten:$flatten -leafsOnly:$leafsOnly -separator $separator -reservedKeys $reservedKeys -baseDir $includePath # Apply prefix if configured $usePrefix = $false if ($includeConfig -is [System.Collections.IDictionary]) { $usePrefix = $includeConfig.prefix -eq $true } foreach ($entry in $includedEntries.GetEnumerator()) { if ($usePrefix) { $key = "$dirName$separator$($entry.Key)" } else { $key = $entry.Key } $result[$key] = $entry.Value } } return $result } function Get-EntryCompletion( [ValidateScript({ $_ -is [System.Collections.IDictionary] })] $map, [ValidateSet("build", "conf")] $language, $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters ) { # For hierarchical completion, we need both flattened and tree structures $flatList = Get-CompletionList $map -flatten:$true -reservedKeys $script:languages.$language.reservedKeys $treeList = Get-CompletionList $map -flatten:$false -reservedKeys $script:languages.$language.reservedKeys # Combine both lists and remove duplicates $allKeys = @($flatList.Keys) + @($treeList.Keys) | Sort-Object -Unique return $allKeys | ? { $_.startswith($wordToComplete) } } function Get-EntryDynamicParam( [System.Collections.IDictionary] $map, $key, $command, [int]$skip = 0, $bound ) { if (!$key) { return @() } $selectedEntry = Get-MapEntry $map $key if (!$selectedEntry) { return @() } # Use the command parameter to determine which command to extract, defaulting to "exec" $commandKey = $command ? $command : "exec" $entryCommand = Get-EntryCommand $selectedEntry $commandKey if (!$entryCommand) { return @() } $p = Get-ScriptArgs $entryCommand -skip $skip return $p } function Invoke-EntryCommand { [OutputType([System.Management.Automation.RuntimeDefinedParameterDictionary])] param( [scriptblock]$func, [int]$skip = 0 ) function Get-SingleArg { [OutputType([System.Management.Automation.RuntimeDefinedParameter])] param([System.Management.Automation.Language.ParameterAst] $ast) $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] $paramAttribute = New-Object -Type System.Management.Automation.ParameterAttribute $paramAttributesCollect.Add($paramAttribute) $paramType = $ast.StaticType foreach ($attr in $ast.Attributes) { if ($attr -is [System.Management.Automation.Language.TypeConstraintAst]) { if ($attr.TypeName.ToString() -eq "switch") { $paramType = [switch] } else { # $newAttr = New-Object -type System.Management.Automation.PSTypeNameAttribute($attr.TypeName.Name) # $paramAttributesCollect.Add($newAttr) } } } # Create parameter with name, type, and attributes $name = $ast.Name.ToString().Trim("`$") $dynParam = New-Object -Type System.Management.Automation.RuntimeDefinedParameter($name, $paramType, $paramAttributesCollect) return $dynParam } $exclude = @("`$_context", "`$_self") # Add parameter to parameter dictionary and return the object $paramDictionary = New-Object ` -Type System.Management.Automation.RuntimeDefinedParameterDictionary # Check if ParamBlock exists before accessing Parameters if ($func.AST.ParamBlock -and $func.AST.ParamBlock.Parameters) { $parameters = $func.AST.ParamBlock.Parameters $skipped = 0 foreach ($param in $parameters) { if ("$($param.Name)" -in $exclude) { continue } if ($skipped -lt $skip) { $skipped++ continue } $dynParam = Get-SingleArg $param $paramDictionary.Add($dynParam.Name, $dynParam) } } return $paramDictionary } function Get-ScriptArgs { [OutputType([System.Management.Automation.RuntimeDefinedParameterDictionary])] param( [scriptblock]$func, [int]$skip = 0 ) function Get-SingleArg { [OutputType([System.Management.Automation.RuntimeDefinedParameter])] param([System.Management.Automation.Language.ParameterAst] $ast) $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] $paramAttribute = New-Object -Type System.Management.Automation.ParameterAttribute $paramAttributesCollect.Add($paramAttribute) $paramType = $ast.StaticType foreach ($attr in $ast.Attributes) { if ($attr -is [System.Management.Automation.Language.TypeConstraintAst]) { if ($attr.TypeName.ToString() -eq "switch") { $paramType = [switch] } else { # $newAttr = New-Object -type System.Management.Automation.PSTypeNameAttribute($attr.TypeName.Name) # $paramAttributesCollect.Add($newAttr) } } } # Create parameter with name, type, and attributes $name = $ast.Name.ToString().Trim("`$") $dynParam = New-Object -Type System.Management.Automation.RuntimeDefinedParameter($name, $paramType, $paramAttributesCollect) return $dynParam } $exclude = @("`$_context", "`$_self") # Add parameter to parameter dictionary and return the object $paramDictionary = New-Object ` -Type System.Management.Automation.RuntimeDefinedParameterDictionary # Check if ParamBlock exists before accessing Parameters if ($func.AST.ParamBlock -and $func.AST.ParamBlock.Parameters) { $parameters = $func.AST.ParamBlock.Parameters $skipped = 0 foreach ($param in $parameters) { if ("$($param.Name)" -in $exclude) { continue } if ($skipped -lt $skip) { $skipped++ continue } $dynParam = Get-SingleArg $param $paramDictionary.Add($dynParam.Name, $dynParam) } } return $paramDictionary } |