posh-HumpCompletion.ps1
$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 return $true; } else { DebugMessage -message "loading command cache - wait on async load" if ($script:iar.IsCompleted) { $tempResult = $script:powershell.EndInvoke($script:iar) DebugMessage -message "loading command cache - got tempResult.result.Keys.Count: $($tempResult.result.Keys.Count)" $global:HumpCompletionCommandCache = $tempResult.result $script:Powershell.Dispose() $script:Runspace.Close() $script:Runspace = $null DebugMessage -message "loading command cache - async load commplete $($global:HumpCompletionCommandCache.Keys.Count)" return $true; } else { return $false; } } } else { return $true } } function local:LoadHumpCompletionCommandCacheAsync(){ DebugMessage -message "LoadHumpCompletionCommandCacheAsync" if ($script:Runspace -eq $null) { DebugMessage -message "LoadHumpCompletionCommandCacheAsync - starting..." $script:Runspace = [RunspaceFactory]::CreateRunspace() $script:Runspace.Open() $null = $script:Runspace.SessionStateProxy.Path.SetLocation($PSScriptRoot); ## set working dir for runspace # 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 [void]$script:Powershell.AddScript( { try { # working dir is set for the runspace . "$pwd/common.ps1" DebugMessage -message "AsyncLoad: Starting..." $slCommands = GetCommandsWithVerbAndHumpSuffix DebugMessage -message "AsyncLoad: Got $($slCommands.Keys.Count) Keys" $wrappedResult = @{ "result" = $slCommands} # work around group enumeration as it loses the grouping! return $wrappedResult } catch { DebugMessage -message "!!!!exception in async load block" return @{ "result" = "!!exception"} } }) $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) if (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 } } else { DebugMessage -message "No command cache" } } 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 |