posh-HumpCompletion.ps1
function local:DebugMessage($message) { # $threadId = [System.Threading.Thread]::CurrentThread.ManagedThreadId # $appDomainId = [AppDomain]::CurrentDomain.Id # [System.Diagnostics.Debug]::WriteLine("PoshHump: $threadId : $appDomainId :$message") [System.Diagnostics.Debug]::WriteLine("PoshHump: $message") } function local:GetCommandWithVerbAndHumpSuffix($commandName) { $separatorIndex = $commandName.IndexOf('-') if ($separatorIndex -ge 0){ $verb = $commandName.SubString(0, $separatorIndex) $suffix = $commandName.SubString($separatorIndex+1) return [PSCustomObject] @{ "Verb" = $verb "Suffix" = $suffix "SuffixHumpForm" = $suffix -creplace "[a-z]","" # case sensitive replace "Command" = $commandName } } } function local:GetCommandsWithVerbAndHumpSuffix() { $commandsGroupedByVerb = Get-Command ` | ForEach-Object { GetCommandWithVerbAndHumpSuffix $_.Name} ` | Group-Object Verb $commands = @{} $commandsGroupedByVerb | ForEach-Object { $commands[$_.Name] = $_.Group | group-object SuffixHumpForm } return $commands } function local:GetWildcardForm($suffix){ # create a wildcard form of a suffix. E.g. for "AzRGr" return "Az*R*Gr*" if ($suffix -eq $null -or $suffix.Length -eq 0){ return "*" } $startIndex = 1; $result = $suffix[0] if ($suffix[0] -eq '-'){ $result += $suffix[1] $startIndex = 2 } for($i=$startIndex ; $i -lt $suffix.Length ; $i++){ if ([char]::IsUpper($suffix[$i])) { $result += "*" } $result += $suffix[$i] } $result += "*" return $result } $Runspace = $null $Powershell = $null function local:EnsureHumpCompletionCommandCache(){ if ($global:HumpCompletionCommandCache -eq $null) { if ($script:runspace -eq $null) { DebugMessage -message "loading command cache" $global:HumpCompletionCommandCache = GetCommandsWithVerbAndHumpSuffix } else { DebugMessage -message "loading command cache - wait on async load" $foo = $script:Runspace.AsyncWaitHandle.WaitOne() $global:HumpCompletionCommandCache = $script:powershell.EndInvoke($script:iar).result $script:Powershell.Dispose() $script:Runspace.Close() $script:Runspace = $null DebugMessage -message "loading command cache - async load commplete $($global:HumpCompletionCommandCache.Count)" } } } function local:LoadHumpCompletionCommandCacheAsync(){ DebugMessage -message "LoadHumpCompletionCommandCacheAsync" if ($script:Runspace -eq $null) { DebugMessage -message "LoadHumpCompletionCommandCacheAsync - starting..." $script:Runspace = [RunspaceFactory]::CreateRunspace() $script:Runspace.Open() # Set variable to prevent installation of the TabExpansion function in the created runspace # Otherwise we end up recursively spinning up runspaces to load the commands! $script:Runspace.SessionStateProxy.SetVariable('poshhumpSkipTabCompletionInstall',$true) $script:Powershell = [PowerShell]::Create() $script:Powershell.Runspace = $script:Runspace $scriptBlock = { $result = GetCommandsWithVerbAndHumpSuffix @{ "result" = $result} # work around group enumeration as it loses the grouping! } $script:Powershell.AddScript($scriptBlock) | out-null $script:iar = $script:PowerShell.BeginInvoke() } } function local:GetParameters($commandName){ $command = Get-Command $commandName -ShowCommandInfo if ($command.CommandType -eq "Alias") { $command = Get-Command $command.Definition -ShowCommandInfo } # TODO - look at whether we can determine the parameter set to be smarter about the parameters we complete $command.ParameterSets ` | Select-Object -ExpandProperty Parameters ` | Select-Object -ExpandProperty Name -Unique ` | ForEach-Object { "-$($_)" } ` | Sort-Object } function local:PoshHumpTabExpansion2( [System.Management.Automation.Language.Ast]$ast, [int]$offset){ $result = $null; DebugMessage "***** In PoshHumpTabExpansion2 - offset $offset" $statements = $ast.EndBlock.Statements $command = $statements.PipelineElements[$statements.PipelineElements.Count-1] if ($command -is [System.Management.Automation.Language.CommandAst]){ $commandName = $command.GetCommandName() } else { $commandName = $null } DebugMessage "Command name: $commandName" # We want to find any NamedAttributeArgumentAst objects where the Ast extent includes $offset $offsetInExtentPredicate = { param($astToTest) return $offset -gt $astToTest.Extent.StartOffset -and $offset -le $astToTest.Extent.EndOffset } $asts = $ast.FindAll($offsetInExtentPredicate, $true) $astCount = $asts.Count; $msg = ($asts | %{ $_.GetType().Name}) -join ", " DebugMessage "AstsInExtent ($astCount): $msg" if ($astCount -gt 2 ` -and $asts[$astCount-2] -is [System.Management.Automation.Language.CommandAst] ` -and $asts[$astCount-1] -is [System.Management.Automation.Language.StringConstantExpressionAst]) { # AST chain ends with CommandAst, StringConstantExpressionAst $result = PoshHumpTabExpansion2_Command $asts } elseif ($astCount -gt 2 ` -and $asts[$astCount-2] -is [System.Management.Automation.Language.CommandAst] ` -and $asts[$astCount-1] -is [System.Management.Automation.Language.CommandParameterAst]){ $result = PoshHumpTabExpansion2_Parameter $asts } elseif($astCount -ge 1 ` -and $asts[$astCount-1] -is [System.Management.Automation.Language.VariableExpressionAst]) { $result = PoshHumpTabExpansion2_Variable $asts } $msg = ($result.CompletionMatches) -join ", " DebugMessage "Returning: Count=$($result.CompletionMatches.Length), values=$msg" return $result } function local:PoshHumpTabExpansion2_Command($asts){ $astCount = $asts.Count; $commandAst = $asts[$astCount-2] $stringAst = $asts[$astCount-1] $extentStart = $stringAst.Extent.StartOffset $extentEnd = $stringAst.Extent.EndOffset DebugMessage "CommandAst match: '$($commandAst.CommandElements.Value)' - $($extentStart):$($extentEnd)" $commandName = $ast.ToString().Substring($extentStart, $extentEnd - $extentStart) EnsureHumpCompletionCommandCache $commandInfo = GetCommandWithVerbAndHumpSuffix $commandName $verb = $commandInfo.Verb $suffix= $commandInfo.Suffix $suffixWildcardForm = GetWildcardForm $suffix $wildcardForm = "$verb-$suffixWildcardForm" DebugMessage "CommandName: '$commandName', wildcardForm: '$wildcardForm'" $commands = $global:HumpCompletionCommandCache if ($commands[$verb] -ne $null) { $completionMatches = $commands[$verb] ` | Where-Object { # $_.Name is suffix hump form # Match on hump form of completion word $_.Name.StartsWith($commandInfo.SuffixHumpForm) } ` | Select-Object -ExpandProperty Group ` | Where-Object { $_.Suffix -clike $suffixWildcardForm } ` | Select-Object -ExpandProperty Command ` | Sort-Object $msg = $completionMatches -join ", " DebugMessage "cmd: Count=$($completionMatches.Length), values=$msg" $result = [PSCustomObject]@{ ReplacementIndex = $stringAst.Extent.StartOffset; ReplacementLength = $stringAst.Extent.EndOffset - $stringAst.Extent.StartOffset; CompletionMatches = $completionMatches }; return $result } } function local:PoshHumpTabExpansion2_Parameter($asts){ $commandAst = $asts[$astCount-2] $parameterAst = $asts[$astCount-1] $extentStart = $parameterAst.Extent.StartOffset $extentEnd = $parameterAst.Extent.EndOffset DebugMessage "ParameterAst match: '$($commandAst.CommandElements.Value)' - $($extentStart):$($extentEnd)" $commandName = $commandAst.CommandElements.Value $parameterName = $ast.ToString().Substring($extentStart, $extentEnd - $extentStart) $wildcardForm = GetWildcardForm $parameterName DebugMessage "ParameterName: '$parameterName', wildcardForm: '$wildcardForm'" $parameters = GetParameters -commandName $commandName $completionMatches = $parameters ` | Where-Object { # DebugMessage "Match test '$_', '$wildcardForm', match $($_ -clike $wildcardForm)" $_ -clike $wildcardForm } $result = [PSCustomObject]@{ ReplacementIndex = $extentStart; ReplacementLength = $extentEnd - $extentStart; CompletionMatches = $completionMatches }; return $result } function local:PoshHumpTabExpansion2_Variable($asts){ DebugMessage "************* variable completion *****************" $variableAst = $asts[$astCount-1] $extentStart = $variableAst.Extent.StartOffset $extentEnd = $variableAst.Extent.EndOffset $variableNameWithPrefix = $ast.ToString().Substring($extentStart, $extentEnd - $extentStart) $variableName = $variableNameWithPrefix.TrimStart("`$") $wildcardForm = GetWildcardForm $variableName DebugMessage "VariableAst match: '$variableName' - $($extentStart):$($extentEnd)" $completionMatches = Get-Variable ` | Select-Object -ExpandProperty Name ` | Where-Object { # DebugMessage "==== '$_' -clike '$wildcardForm' == $($_ -clike $wildcardForm)" $_ -clike $wildcardForm } ` | Foreach-Object { "`$$_" } $result = [PSCustomObject]@{ ReplacementIndex = $extentStart; ReplacementLength = $extentEnd - $extentStart; CompletionMatches = $completionMatches }; return $result } function Clear-HumpCompletionCommandCache() { [Cmdletbinding()] param() DebugMessage -message "PoshHumpTabExpansion:clearing command cache" $global:HumpCompletionCommandCache = $null } function Stop-HumpCompletion(){ [Cmdletbinding()] param() $global:HumpCompletionEnabled = $false } function Start-HumpCompletion(){ [Cmdletbinding()] param() $global:HumpCompletionEnabled = $true } # install the handler! DebugMessage -message "Installing: Test PoshHumpTabExpansion2Backup function" if ($poshhumpSkipTabCompletionInstall){ DebugMessage -message "Skipping tab expansion installation" } else { if (-not (Test-Path Function:\PoshHumpTabExpansion2Backup)) { if (Test-Path Function:\TabExpansion2) { DebugMessage -message "Installing: Backup TabExpansion2 function" Rename-Item Function:\TabExpansion2 PoshHumpTabExpansion2Backup } function global:TabExpansion2(){ <# Options include: RelativeFilePaths - [bool] Always resolve file paths using Resolve-Path -Relative. The default is to use some heuristics to guess if relative or absolute is better. To customize your own custom options, pass a hashtable to CompleteInput, e.g. return [System.Management.Automation.CommandCompletion]::CompleteInput($inputScript, $cursorColumn, @{ RelativeFilePaths=$false } #> [CmdletBinding(DefaultParameterSetName = 'ScriptInputSet')] Param( [Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 0)] [string] $inputScript, [Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 1)] [int] $cursorColumn, [Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 0)] [System.Management.Automation.Language.Ast] $ast, [Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 1)] [System.Management.Automation.Language.Token[]] $tokens, [Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 2)] [System.Management.Automation.Language.IScriptPosition] $positionOfCursor, [Parameter(ParameterSetName = 'ScriptInputSet', Position = 2)] [Parameter(ParameterSetName = 'AstInputSet', Position = 3)] [Hashtable] $options = $null ) End { if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet') { $results = [System.Management.Automation.CommandCompletion]::CompleteInput( <#inputScript#> $inputScript, <#cursorColumn#> $cursorColumn, <#options#> $options) } else { $results = [System.Management.Automation.CommandCompletion]::CompleteInput( <#ast#> $ast, <#tokens#> $tokens, <#positionOfCursor#> $positionOfCursor, <#options#> $options) } if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet') { $ast = [System.Management.Automation.Language.Parser]::ParseInput($inputScript, [ref]$tokens, [ref]$null) # DebugMessage "Ast: $($ast.ToString())" } else { $cursorColumn = $positionOfCursor.Offset } $poshHumpResult = PoshHumpTabExpansion2 $ast $cursorColumn if ($poshHumpResult -ne $null){ $results.ReplacementIndex = $poshHumpResult.ReplacementIndex $results.ReplacementLength = $poshHumpResult.ReplacementLength # From TabExpansionPlusPlus: Workaround where PowerShell returns a readonly collection that we need to add to. if ($results.CompletionMatches.IsReadOnly) { $collection = new-object System.Collections.ObjectModel.Collection[System.Management.Automation.CompletionResult] $results.GetType().GetProperty('CompletionMatches').SetValue($results, $collection) } # $results.CompletionMatches.Clear() # TODO - look at inserting at front instead of clearing as this removes standard completion! Augment vs override $poshHumpResult.CompletionMatches | % { $results.CompletionMatches.Add($_)} } return $results } } LoadHumpCompletionCommandCacheAsync } } $global:HumpCompletionEnabled = $true |