private/functions/Get-PowerStubCompletion.ps1

<#
.SYNOPSIS
  Tab completion for pstb, Invoke-PowerStubCommand and direct aliases.

.DESCRIPTION
  Called by the module's TabExpansion2 wrapper. Returns $null for anything that is not a
  PowerStub call, and the wrapper then uses PowerShell's normal completion.

  Invoke-PowerStubCommand declares no parameters (see the comment in that function), so
  PowerShell has nothing to complete from. This function supplies it instead:

  - stub position: registered stubs and the virtual verbs
  - command position: the stub's commands (stub names after 'help' and 'update')
  - after that: the line is rewritten to the direct call, & '<target path>' ..., and
                      handed to PowerShell's own completion. The user therefore gets exactly
                      what they would get calling the target directly: its parameters,
                      ValidateSet and enum values, paths, and so on.

.PARAMETER InputScript
  The whole line being completed.

.PARAMETER CursorColumn
  The cursor offset into InputScript.

.PARAMETER Options
  Completion options, passed through to PowerShell's completion.

.INPUTS
None. You cannot pipe objects to this function.

.OUTPUTS
System.Management.Automation.CommandCompletion, or nothing.
#>



function Get-PowerStubCompletion {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string] $InputScript,

        [Parameter(Mandatory)]
        [int] $CursorColumn,

        [hashtable] $Options
    )

    # A host can report the cursor one past the text (the end-of-input token sits there)
    $CursorColumn = [Math]::Max(0, [Math]::Min($CursorColumn, $InputScript.Length))

    $tokens = $null
    $parseErrors = $null
    $ast = [System.Management.Automation.Language.Parser]::ParseInput($InputScript, [ref]$tokens, [ref]$parseErrors)

    # The innermost command the cursor is in. For 'pstb S c -Name (Get-Date ' that is Get-Date,
    # which is not ours, so normal completion handles it.
    $commandAst = $ast.FindAll({
            param($node)
            $node -is [System.Management.Automation.Language.CommandAst] -and $node.Extent.StartOffset -le $CursorColumn
        }, $true) | Sort-Object { $_.Extent.StartOffset } | Select-Object -Last 1
    if (-not $commandAst) { return }

    # The cursor must be inside the command, or separated from its end only by whitespace
    if ($CursorColumn -gt $commandAst.Extent.EndOffset) {
        $gap = $InputScript.Substring($commandAst.Extent.EndOffset, $CursorColumn - $commandAst.Extent.EndOffset)
        if ($gap.Trim()) { return }
    }

    $commandName = $commandAst.GetCommandName()
    if (-not $commandName) { return }

    $aliasStub = $null
    if ($commandName -ne 'Invoke-PowerStubCommand' -and $commandName -ne $Script:InvokeAlias) {
        if ($Script:RegisteredDirectAliases -notcontains $commandName) { return }
        $aliasStub = (Get-PowerStubConfigurationKey 'DirectAliases')[$commandName]
        if (-not $aliasStub) { return }
    }

    Sync-PowerStubConfiguration

    # Split the arguments into those completely before the cursor and the one being typed
    $elements = @($commandAst.CommandElements | Select-Object -Skip 1)
    $current = $elements | Where-Object { $_.Extent.StartOffset -le $CursorColumn -and $CursorColumn -le $_.Extent.EndOffset } | Select-Object -First 1
    $before = @($elements | Where-Object { $_.Extent.EndOffset -lt $CursorColumn -and $_ -ne $current })

    $replaceStart = if ($current) { $current.Extent.StartOffset } else { $CursorColumn }
    $word = $InputScript.Substring($replaceStart, $CursorColumn - $replaceStart)

    $beforeTokens = @($before | ForEach-Object {
            if ($_ -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $_.Value } else { $_.Extent.Text }
        })
    $invocation = Resolve-PowerStubInvocation -Tokens $beforeTokens -StubIsKnown:([bool]$aliasStub)
    $position = $before.Count

    $stub = if ($aliasStub) { $aliasStub } elseif ($invocation.StubIndex -ge 0 -and $invocation.StubIndex -lt $position) { $beforeTokens[$invocation.StubIndex] }
    $command = if ($invocation.CommandIndex -ge 0 -and $invocation.CommandIndex -lt $position) { $beforeTokens[$invocation.CommandIndex] }

    $virtualVerbs = @('search', 'help', 'update')
    $stubNames = @((Get-PowerStubConfigurationKey 'Stubs').Keys | Sort-Object)

    $candidates = $null
    if (-not $stub) {
        if ($word.StartsWith('-')) { return }
        $candidates = $virtualVerbs + $stubNames
    }
    elseif (-not $command) {
        if ($word.StartsWith('-')) { return }
        $candidates = switch ($stub) {
            'search' { return }
            { $_ -in 'help', 'update' } { $stubNames; break }
            default { Get-PowerStubCompletionCommandName -Stub $stub }
        }
    }
    elseif ($stub -eq 'help' -and $position -eq $invocation.RestStart) {
        # pstb help <stub> <command>
        $candidates = Get-PowerStubCompletionCommandName -Stub $command
    }
    elseif ($stub -in $virtualVerbs) {
        return
    }

    if ($null -ne $candidates) {
        $matching = @($candidates | Where-Object { $_ -like "$word*" })
        if (-not $matching) { return }

        $results = [System.Collections.ObjectModel.Collection[System.Management.Automation.CompletionResult]]::new()
        foreach ($text in $matching) {
            $results.Add([System.Management.Automation.CompletionResult]::new($text, $text, 'ParameterValue', $text))
        }
        return [System.Management.Automation.CommandCompletion]::new($results, -1, $replaceStart, $word.Length)
    }

    # The target's own arguments: complete them as if the target had been called directly
    if (-not $Script:OriginalTabExpansion2) { return }
    $target = Get-PowerStubCommand $stub $command -WarningAction SilentlyContinue
    if (-not $target) { return }

    $argsStart = if ($invocation.RestStart -lt $elements.Count) { $elements[$invocation.RestStart].Extent.StartOffset } else { $CursorColumn }
    $directCall = "& '$($target.Path -replace "'", "''")' "
    $commandStart = $commandAst.Extent.StartOffset
    $rewritten = $InputScript.Substring(0, $commandStart) + $directCall + $InputScript.Substring($argsStart)
    $shift = ($commandStart + $directCall.Length) - $argsStart

    $completion = & $Script:OriginalTabExpansion2 -inputScript $rewritten -cursorColumn ($CursorColumn + $shift) -options $Options
    if ($completion) {
        $completion.ReplacementIndex -= $shift
    }
    return $completion
}