completers/wpr_completer/wpr_completer.ps1

<#
.SYNOPSIS
    Registers a native PowerShell argument completer for wpr.

.DESCRIPTION
    Provides a static-first native completer for `wpr` and `wpr.exe` using the
    local help surface and safe local discovery for profile names and process data.

    The completer covers:
    - top-level WPR commands
    - `-help` topic completion
    - profile-name completion for profile-bearing commands
    - local file and directory completion for path-bearing commands
    - process name and PID completion for heap and snapshot commands
    - enum and placeholder values for common non-path slots

    The script keeps its top level compatible with `Import-CompleterScript`.
#>


Set-StrictMode -Version Latest

function New-WprCompletionResult {
    param(
        [string]$CompletionText,
        [string]$ResultType = 'ParameterValue',
        [string]$ToolTip,
        [string]$ListItemText
    )

    if ([string]::IsNullOrWhiteSpace($ListItemText)) {
        $ListItemText = $CompletionText
    }

    if ([string]::IsNullOrWhiteSpace($ToolTip)) {
        $ToolTip = $ListItemText
    }

    [System.Management.Automation.CompletionResult]::new(
        $CompletionText,
        $ListItemText,
        $ResultType,
        $ToolTip
    )
}

function Get-WprCommandSpecs {
    if (Get-Variable -Name WprCommandSpecs -Scope Script -ErrorAction Ignore) {
        return $script:WprCommandSpecs
    }

    $script:WprCommandSpecs = @(
        [pscustomobject]@{ Token='/?'; Description='Show WPR help.'; Options=@(); ValueKind=$null }
        [pscustomobject]@{ Token='-?'; Description='Show WPR help.'; Options=@(); ValueKind=$null }
        [pscustomobject]@{ Token='-help'; Description='Show WPR help for a specific topic.'; Options=@(); ValueKind='HelpTopic' }
        [pscustomobject]@{ Token='-start'; Description='Start WPR recording with one or more profiles.'; Options=@('-start','-shutdown','-filemode','-recordtempto','-container','-host','-instancename'); ValueKind='Start' }
        [pscustomobject]@{ Token='-stop'; Description='Stop WPR recording and merge it into the given file.'; Options=@('-skipPdbGen','-force','-compress','-container','-host','-instancename'); ValueKind='Stop' }
        [pscustomobject]@{ Token='-cancel'; Description='Cancel the recording initiated through WPR.'; Options=@('-container','-host','-instancename'); ValueKind='Cancel' }
        [pscustomobject]@{ Token='-merge'; Description='Merge one or more ETL files into the given file.'; Options=@('-skipPdbGen','-compress','-supresspii','-mergeonly','-injectonly'); ValueKind='Merge' }
        [pscustomobject]@{ Token='-status'; Description='Display the status of the active WPR recording.'; Options=@('-details','-container','-host','-instancename'); ValueKind='Status' }
        [pscustomobject]@{ Token='-profiles'; Description='Enumerate built-in or custom profile names.'; Options=@(); ValueKind='Profiles' }
        [pscustomobject]@{ Token='-profiledetails'; Description='Display details about one or more profiles.'; Options=@('-filemode'); ValueKind='ProfileDetails' }
        [pscustomobject]@{ Token='-exportprofile'; Description='Export one or more built-in profiles to a .wprp file.'; Options=@('-filemode'); ValueKind='ExportProfile' }
        [pscustomobject]@{ Token='-providers'; Description='Display provider information.'; Options=@(); ValueKind='Providers' }
        [pscustomobject]@{ Token='-marker'; Description='Fire an event marker.'; Options=@('-flush'); ValueKind='Marker' }
        [pscustomobject]@{ Token='-flush'; Description='Flush logging sessions initiated through WPR.'; Options=@('-container','-host','-instancename'); ValueKind='Flush' }
        [pscustomobject]@{ Token='-capturestateondemand'; Description='Capture provider states in the current recording.'; Options=@('-instancename'); ValueKind='CaptureStateOnDemand' }
        [pscustomobject]@{ Token='-addboot'; Description='Enable Autologger for the given profile.'; Options=@('-addboot','-filemode','-recordtempto','-export'); ValueKind='AddBoot' }
        [pscustomobject]@{ Token='-stopboot'; Description='Stop boot recording and merge it into the given file.'; Options=@(); ValueKind='StopBoot' }
        [pscustomobject]@{ Token='-cancelboot'; Description='Cancel boot recording configured by addboot.'; Options=@(); ValueKind='CancelBoot' }
        [pscustomobject]@{ Token='-heaptracingconfig'; Description='Enable, disable, or query heap tracing for a process or store app.'; Options=@(); ValueKind='HeapTracingConfig' }
        [pscustomobject]@{ Token='-snapshotconfig'; Description='Enable, disable, or query snapshot capture for a process.'; Options=@('-name','-pid'); ValueKind='SnapshotConfig' }
        [pscustomobject]@{ Token='-enableperiodicsnapshot'; Description='Enable periodic snapshot for specified PIDs.'; Options=@(); ValueKind='EnablePeriodicSnapshot' }
        [pscustomobject]@{ Token='-disableperiodicsnapshot'; Description='Disable periodic snapshot.'; Options=@(); ValueKind='DisablePeriodicSnapshot' }
        [pscustomobject]@{ Token='-singlesnapshot'; Description='Take an on-demand snapshot for specified PIDs.'; Options=@(); ValueKind='SingleSnapshot' }
        [pscustomobject]@{ Token='-pmcsources'; Description='Query hardware counters available on the system.'; Options=@(); ValueKind='None' }
        [pscustomobject]@{ Token='-pmcsessions'; Description='Query sessions using hardware counters.'; Options=@(); ValueKind='None' }
        [pscustomobject]@{ Token='-setprofint'; Description='Set sampled profile interval.'; Options=@(); ValueKind='ProfileInterval' }
        [pscustomobject]@{ Token='-profint'; Description='Query the current profile interval.'; Options=@(); ValueKind='None' }
        [pscustomobject]@{ Token='-resetprofint'; Description='Reset profile interval to its default value.'; Options=@(); ValueKind='ResetProfileInterval' }
        [pscustomobject]@{ Token='-purgecache'; Description='Purge the dynamic symbols cache.'; Options=@(); ValueKind='None' }
        [pscustomobject]@{ Token='-log'; Description='Configure WPR debug logging to the event log.'; Options=@(); ValueKind='LogMode' }
        [pscustomobject]@{ Token='-disablepagingexecutive'; Description='Turn Disable Paging Executive on or off.'; Options=@(); ValueKind='PagingExecutive' }
    )

    $script:WprCommandSpecs
}

function Get-WprCommandLookup {
    if (Get-Variable -Name WprCommandLookup -Scope Script -ErrorAction Ignore) {
        return $script:WprCommandLookup
    }

    $lookup = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($spec in @(Get-WprCommandSpecs)) {
        $lookup[$spec.Token] = $spec
    }

    $script:WprCommandLookup = $lookup
    $script:WprCommandLookup
}

function Get-WprHelpTopics {
    @('start', 'stop', 'status', 'profiles', 'providers', 'tracing', 'boottrace', 'heap', 'hardwarecounter', 'advanced')
}

function Get-WprStatusTerms {
    @('profiles', 'collectors')
}

function Get-WprProviderSelectors {
    @('Installed', 'I', 'PerfTrack', 'PT', 'UTC')
}

function Get-WprSnapshotOptions {
    @('Heap')
}

function Get-WprLogModes {
    @('enabled', 'disabled', 'remove')
}

function Get-WprPagingModes {
    @('on', 'off')
}

function Get-WprEnableDisableValues {
    @('enable', 'disable')
}

function Get-WprTokenState {
    param(
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition
    )

    # Words come from the parser, so quoted words (ASCII or typographic, with escapes) stay
    # whole; an unterminated quoted word is one element running to the cursor.
    $tokens = New-Object System.Collections.Generic.List[string]
    $currentToken = ''
    foreach ($element in $CommandAst.CommandElements) {
        $extent = $element.Extent
        if ($extent.StartOffset -ge $CursorPosition) {
            break
        }

        if ($CursorPosition -le $extent.EndOffset) {
            $currentToken = $extent.Text.Substring(0, $CursorPosition - $extent.StartOffset)
            break
        }

        $tokens.Add($extent.Text)
    }

    [pscustomobject]@{ TokensBeforeCurrent = @($tokens); CurrentToken = $currentToken }
}

function Get-WprArgumentsFromTokenState {
    param([pscustomobject]$TokenState)

    $tokensBeforeCurrent = @($TokenState.TokensBeforeCurrent)
    $currentArgument = if ($null -eq $TokenState.CurrentToken) { '' } else { $TokenState.CurrentToken }

    if ($tokensBeforeCurrent.Count -gt 0) {
        $argumentsBeforeCurrent = @($tokensBeforeCurrent | Select-Object -Skip 1)
    } else {
        $argumentsBeforeCurrent = @()
    }

    if ($tokensBeforeCurrent.Count -eq 0 -and $currentArgument -match '^(?i)wpr(?:\.exe)?$') {
        $currentArgument = ''
    }

    [pscustomobject]@{
        ArgumentsBeforeCurrent = $argumentsBeforeCurrent
        CurrentArgument        = $currentArgument
    }
}

function ConvertFrom-WprTypedWord {
    # The value of a typed word. A word opened with a quote (ASCII or typographic) is read by
    # the PowerShell tokenizer, which drops the quotes and undoes that quote style's escapes.
    param([string]$Value)

    if ($Value -notmatch '^[''"\u2018-\u201E]') {
        return $Value
    }

    $tokens = $null
    $parseErrors = $null
    [void][System.Management.Automation.Language.Parser]::ParseInput($Value, [ref]$tokens, [ref]$parseErrors)
    $tokens[0].Value
}

function Get-WprTypedQuote {
    param([string]$Value)

    if ($Value -match '^[''"\u2018-\u201E]') {
        return $Value.Substring(0, 1)
    }

    ''
}

function ConvertTo-WprQuotedValue {
    # Renders a value as one PowerShell argument: bare when safe and no quote was typed,
    # otherwise in the quote the user typed (single by default). PowerShell reads ' and
    # U+2018-U+201B as single quotes and " and U+201C-U+201E as double quotes.
    param(
        [string]$Value,
        [string]$QuoteChar = ''
    )

    if ([string]::IsNullOrEmpty($Value)) {
        return $Value
    }

    if (-not $QuoteChar) {
        # A bare word opened with a dash would be read as a parameter.
        if ($Value -notmatch '[\s{}();,|&<>''"`$@#\u2018-\u201E]|^[-\u2013-\u2015]') {
            return $Value
        }

        $QuoteChar = "'"
    }

    if ($QuoteChar -match '^[''\u2018-\u201B]$') {
        return $QuoteChar + ($Value -replace '([''\u2018-\u201B])', '$1$1') + $QuoteChar
    }

    $QuoteChar + ($Value -replace '([`"$\u201C-\u201E])', '`$1') + $QuoteChar
}

function Get-WprPathCompletions {
    param(
        [string]$CurrentWord,
        [ValidateSet('File','Directory','Any')]
        [string]$Kind = 'Any',
        [string]$ToolTip = 'Path value.',
        [string]$Placeholder = '<path>'
    )

    $typedValue = ConvertFrom-WprTypedWord -Value $CurrentWord
    $quoteChar = Get-WprTypedQuote -Value $CurrentWord
    $results = New-Object System.Collections.Generic.List[object]

    # The typed directory part (through the last separator, or a bare drive 'C:') is kept
    # verbatim so a typed .\ or ./ prefix and separator style survive.
    $typedParent = ''
    $leaf = if ($null -eq $typedValue) { '' } else { $typedValue }
    $lastSeparator = $leaf.LastIndexOfAny([char[]]@('\', '/'))
    if ($lastSeparator -ge 0) {
        $typedParent = $leaf.Substring(0, $lastSeparator + 1)
        $leaf = $leaf.Substring($lastSeparator + 1)
    } elseif ($leaf -match '^[A-Za-z]:') {
        $typedParent = $leaf.Substring(0, 2)
        $leaf = $leaf.Substring(2)
    }

    $parentPath = if ($typedParent) { $typedParent } else { '.' }
    $separator = if ($typedParent.EndsWith('/')) { '/' } else { '\' }
    $items = @(Get-ChildItem -LiteralPath $parentPath -ErrorAction Ignore)

    foreach ($item in $items) {
        # Directories are always kept so a file slot can be steered into a subfolder;
        # only leaf items are filtered by Kind.
        if ($Kind -eq 'Directory' -and -not $item.PSIsContainer) {
            continue
        }

        if (-not [string]::IsNullOrWhiteSpace($leaf) -and -not $item.Name.StartsWith($leaf, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        # A bare relative name opening with a dash gets the current-directory prefix, as
        # PowerShell's own file completion does, so it is never read as a parameter.
        $candidate = if (-not $typedParent -and $item.Name -match '^[-\u2013-\u2015]') {
            '.' + [System.IO.Path]::DirectorySeparatorChar + $item.Name
        } else {
            $typedParent + $item.Name
        }
        if ($item.PSIsContainer) {
            $candidate += $separator
        }

        $completionText = ConvertTo-WprQuotedValue -Value $candidate -QuoteChar $quoteChar
        [void]$results.Add((New-WprCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip $item.FullName -ListItemText $candidate))
    }

    if ($results.Count -eq 0) {
        $fallback = if ([string]::IsNullOrWhiteSpace($CurrentWord)) { $Placeholder } else { $CurrentWord }
        [void]$results.Add((New-WprCompletionResult -CompletionText $fallback -ResultType 'ParameterValue' -ToolTip $ToolTip))
    }

    @($results.ToArray())
}

function Get-WprPlaceholderCompletions {
    param(
        [string]$CurrentWord,
        [string]$Placeholder,
        [string]$ToolTip
    )

    if ([string]::IsNullOrWhiteSpace($CurrentWord)) {
        return @(
            New-WprCompletionResult -CompletionText $Placeholder -ResultType 'ParameterValue' -ToolTip $ToolTip
        )
    }

    @(
        New-WprCompletionResult -CompletionText $CurrentWord -ResultType 'ParameterValue' -ToolTip $ToolTip
    )
}

function Get-WprEnumCompletions {
    param(
        [string]$CurrentWord,
        [string[]]$Values,
        [string]$ToolTip
    )

    $typedValue = ConvertFrom-WprTypedWord -Value $CurrentWord
    $quoteChar = Get-WprTypedQuote -Value $CurrentWord
    $results = New-Object System.Collections.Generic.List[object]
    foreach ($value in @($Values)) {
        if (-not [string]::IsNullOrWhiteSpace($typedValue) -and -not $value.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        $completionText = ConvertTo-WprQuotedValue -Value $value -QuoteChar $quoteChar
        [void]$results.Add((New-WprCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip $ToolTip -ListItemText $value))
    }

    if ($results.Count -eq 0) {
        $fallback = if ([string]::IsNullOrWhiteSpace($CurrentWord)) { '<value>' } else { $CurrentWord }
        [void]$results.Add((New-WprCompletionResult -CompletionText $fallback -ResultType 'ParameterValue' -ToolTip $ToolTip))
    }

    @($results.ToArray())
}

function Get-WprUniqueResults {
    param([object[]]$Results)

    $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    $output = New-Object System.Collections.Generic.List[object]
    foreach ($result in @($Results)) {
        if ($null -eq $result) {
            continue
        }

        if ($seen.Add($result.CompletionText)) {
            [void]$output.Add($result)
        }
    }

    @($output.ToArray())
}

function Get-WprCommandCompletions {
    param([string]$CurrentWord)

    foreach ($command in @(Get-WprCommandSpecs)) {
        if (-not [string]::IsNullOrWhiteSpace($CurrentWord) -and -not $command.Token.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        New-WprCompletionResult -CompletionText $command.Token -ResultType 'ParameterName' -ToolTip $command.Description
    }
}

function Get-WprProfileNames {
    if (Get-Variable -Name WprProfileCache -Scope Script -ErrorAction Ignore) {
        $cache = $script:WprProfileCache
        if (((Get-Date) - $cache.UpdatedAt).TotalSeconds -lt 30) {
            return $cache.Values
        }
    }

    $values = New-Object System.Collections.Generic.List[string]
    if (Get-Command -Name wpr.exe -ErrorAction Ignore) {
        try {
            foreach ($line in @(& wpr.exe -profiles 2>$null)) {
                $text = [string]$line
                $match = [regex]::Match($text, '^\s*(?<name>\S+)\s{2,}.+$')
                if (-not $match.Success) {
                    continue
                }

                $name = $match.Groups['name'].Value
                if ([string]::IsNullOrWhiteSpace($name)) {
                    continue
                }

                [void]$values.Add($name)
                [void]$values.Add($name + '.light')
                [void]$values.Add($name + '.verbose')
            }
        } catch {
        }
    }

    $result = @($values | Sort-Object -Unique)
    $script:WprProfileCache = [pscustomobject]@{ UpdatedAt = Get-Date; Values = $result }
    @($result)
}

function Get-WprProfileSpecCompletions {
    param([string]$CurrentWord)

    $typedValue = ConvertFrom-WprTypedWord -Value $CurrentWord
    if ($typedValue -match '^(?<path>.+!)(?<profile>[^!]*)$') {
        $prefix = $matches['path']
        $profilePrefix = $matches['profile']
        $quoteChar = Get-WprTypedQuote -Value $CurrentWord
        $results = New-Object System.Collections.Generic.List[object]
        foreach ($profile in @(Get-WprProfileNames)) {
            if (-not [string]::IsNullOrWhiteSpace($profilePrefix) -and -not $profile.StartsWith($profilePrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
                continue
            }

            # The file part of file.wprp!Profile may need quoting like any other path.
            $completionText = ConvertTo-WprQuotedValue -Value ($prefix + $profile) -QuoteChar $quoteChar
            [void]$results.Add((New-WprCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip 'WPR profile specification.' -ListItemText ($prefix + $profile)))
        }

        if ($results.Count -gt 0) {
            return @($results.ToArray())
        }
    }

    if ($typedValue -match '[\\/]' -or $typedValue.EndsWith('.wprp', [System.StringComparison]::OrdinalIgnoreCase)) {
        return @(Get-WprPathCompletions -CurrentWord $CurrentWord -Kind 'File' -ToolTip 'Path to a .wprp profile definition file.' -Placeholder '<profile.wprp>')
    }

    $quoteChar = Get-WprTypedQuote -Value $CurrentWord
    $results = New-Object System.Collections.Generic.List[object]
    foreach ($profile in @(Get-WprProfileNames)) {
        if (-not [string]::IsNullOrWhiteSpace($typedValue) -and -not $profile.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        $completionText = ConvertTo-WprQuotedValue -Value $profile -QuoteChar $quoteChar
        [void]$results.Add((New-WprCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip 'WPR profile specification.' -ListItemText $profile))
    }

    if ($results.Count -eq 0) {
        $fallback = if ([string]::IsNullOrWhiteSpace($CurrentWord)) { '<profile>' } else { $CurrentWord }
        [void]$results.Add((New-WprCompletionResult -CompletionText $fallback -ResultType 'ParameterValue' -ToolTip 'WPR profile specification.'))
    }

    @($results.ToArray())
}

function Get-WprPlusListCompletions {
    param([string]$CurrentWord)

    $typedValue = ConvertFrom-WprTypedWord -Value $CurrentWord
    $prefix = ''
    $segment = $typedValue
    $lastPlus = if ([string]::IsNullOrEmpty($typedValue)) { -1 } else { $typedValue.LastIndexOf('+') }
    if ($lastPlus -ge 0) {
        $prefix = $typedValue.Substring(0, $lastPlus + 1)
        $segment = $typedValue.Substring($lastPlus + 1)
    }

    $quoteChar = Get-WprTypedQuote -Value $CurrentWord
    $results = New-Object System.Collections.Generic.List[object]
    foreach ($profile in @(Get-WprProfileNames)) {
        if (-not [string]::IsNullOrWhiteSpace($segment) -and -not $profile.StartsWith($segment, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        $completionText = ConvertTo-WprQuotedValue -Value ($prefix + $profile) -QuoteChar $quoteChar
        [void]$results.Add((New-WprCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip 'WPR profile list item.' -ListItemText ($prefix + $profile)))
    }

    if ($results.Count -eq 0) {
        $fallback = if ([string]::IsNullOrWhiteSpace($CurrentWord)) { '<profile[+profile...]>' } else { $CurrentWord }
        [void]$results.Add((New-WprCompletionResult -CompletionText $fallback -ResultType 'ParameterValue' -ToolTip 'WPR profile list item.'))
    }

    @($results.ToArray())
}

function Get-WprProcessNames {
    if (Get-Variable -Name WprProcessNameCache -Scope Script -ErrorAction Ignore) {
        $cache = $script:WprProcessNameCache
        if (((Get-Date) - $cache.UpdatedAt).TotalSeconds -lt 10) {
            return $cache.Values
        }
    }

    $processes = @(Get-Process -ErrorAction SilentlyContinue)
    $values = @(
        @($processes | ForEach-Object { $_.ProcessName + '.exe' }) +
        @($processes | ForEach-Object { $_.ProcessName })
    ) | Sort-Object -Unique

    $script:WprProcessNameCache = [pscustomobject]@{ UpdatedAt = Get-Date; Values = $values }
    @($values)
}

function Get-WprProcessIds {
    if (Get-Variable -Name WprProcessIdCache -Scope Script -ErrorAction Ignore) {
        $cache = $script:WprProcessIdCache
        if (((Get-Date) - $cache.UpdatedAt).TotalSeconds -lt 10) {
            return $cache.Values
        }
    }

    $values = @(
        Get-Process -ErrorAction SilentlyContinue |
            ForEach-Object { [string]$_.Id }
    ) | Sort-Object -Unique

    $script:WprProcessIdCache = [pscustomobject]@{ UpdatedAt = Get-Date; Values = $values }
    @($values)
}

function Get-WprOptionCompletions {
    param(
        [string]$CurrentWord,
        [string[]]$Options
    )

    foreach ($option in @($Options)) {
        if (-not [string]::IsNullOrWhiteSpace($CurrentWord) -and -not $option.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        New-WprCompletionResult -CompletionText $option -ResultType 'ParameterName' -ToolTip $option
    }
}

function Get-WprCommonParameterValueCompletionList {
    param(
        [string]$PreviousToken,
        [string]$CurrentWord
    )

    # -container and -instancename are the common parameters wpr documents for every
    # session-bound command; their values are opaque, so they complete as placeholders.
    switch ($PreviousToken) {
        '-container' {
            return @(Get-WprPlaceholderCompletions -CurrentWord $CurrentWord -Placeholder '<container-id>' -ToolTip 'Container ID.')
        }
        '-instancename' {
            return @(Get-WprPlaceholderCompletions -CurrentWord $CurrentWord -Placeholder '<instance-name>' -ToolTip 'Unique WPR instance name.')
        }
    }

    @()
}

function Test-WprProfileOperandPresent {
    param(
        [string[]]$TokensAfterCommand,
        [string[]]$ValueOptions
    )

    # A token that is neither an option nor an option's value is a profile operand;
    # once one is present, further profiles must be reintroduced with -start/-addboot.
    $tokens = @($TokensAfterCommand)
    for ($index = 0; $index -lt $tokens.Count; $index++) {
        $token = $tokens[$index]
        if ($token.StartsWith('-')) {
            if ($token -eq '-shutdown') {
                if (($index + 1) -lt $tokens.Count -and $tokens[$index + 1] -eq 'KSR') {
                    $index++
                }
            } elseif ($token -in $ValueOptions) {
                $index++
            }

            continue
        }

        return $true
    }

    $false
}

function Get-WprEnumOrPlaceholderCompletions {
    param(
        [string]$CurrentWord,
        [string[]]$EnumValues,
        [string]$Placeholder,
        [string]$ToolTip
    )

    $results = @(Get-WprEnumCompletions -CurrentWord $CurrentWord -Values $EnumValues -ToolTip $ToolTip)
    if ($results.Count -gt 0) {
        return $results
    }

    @(Get-WprPlaceholderCompletions -CurrentWord $CurrentWord -Placeholder $Placeholder -ToolTip $ToolTip)
}

function Complete-Wpr {
    param(
        [string]$wordToComplete,
        [System.Management.Automation.Language.CommandAst]$commandAst,
        [int]$cursorPosition
    )

    $commandLookup = Get-WprCommandLookup
    $tokenState = Get-WprTokenState -CommandAst $commandAst -CursorPosition $cursorPosition
    $argumentState = Get-WprArgumentsFromTokenState -TokenState $tokenState
    $hasTrailingSpace = [string]::IsNullOrEmpty($wordToComplete)

    if ($hasTrailingSpace -and -not [string]::IsNullOrEmpty($argumentState.CurrentArgument)) {
        $currentWord = ''
        $argumentsBeforeCurrent = @($argumentState.ArgumentsBeforeCurrent + $argumentState.CurrentArgument)
    } else {
        $currentWord = if ($null -eq $argumentState.CurrentArgument) { '' } else { $argumentState.CurrentArgument }
        $argumentsBeforeCurrent = @($argumentState.ArgumentsBeforeCurrent)
    }

    $activeCommand = $null
    foreach ($token in @($argumentsBeforeCurrent)) {
        if ($commandLookup.ContainsKey($token)) {
            $activeCommand = $commandLookup[$token]
            break
        }
    }

    if ($null -eq $activeCommand) {
        if ([string]::IsNullOrWhiteSpace($currentWord) -or $currentWord.StartsWith('-') -or $currentWord.StartsWith('/')) {
            return @(Get-WprCommandCompletions -CurrentWord $currentWord)
        }

        return @()
    }

    # The lookup above is case-insensitive, so the index scan must be too (-Stop, -HeapTracingConfig).
    $commandIndex = -1
    for ($index = 0; $index -lt $argumentsBeforeCurrent.Count; $index++) {
        if ([string]::Equals($argumentsBeforeCurrent[$index], $activeCommand.Token, [System.StringComparison]::OrdinalIgnoreCase)) {
            $commandIndex = $index
            break
        }
    }

    $tokensAfterCommand = @(
        if ($commandIndex -ge 0 -and $commandIndex -lt ($argumentsBeforeCurrent.Count - 1)) {
            $argumentsBeforeCurrent[($commandIndex + 1)..($argumentsBeforeCurrent.Count - 1)]
        }
    )

    switch ($activeCommand.ValueKind) {
        'HelpTopic' {
            if ($tokensAfterCommand.Count -eq 0) {
                return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprHelpTopics) -ToolTip 'WPR help topic.')
            }

            return @(Get-WprPlaceholderCompletions -CurrentWord $currentWord -Placeholder ' ' -ToolTip 'No further arguments are valid after -help <topic>.')
        }
        'Start' {
            $valueOptions = @('-start', '-recordtempto', '-container', '-instancename')
            if ($tokensAfterCommand.Count -gt 0) {
                $previousToken = $tokensAfterCommand[-1]
                if ($previousToken -in @('-start')) {
                    return @(Get-WprProfileSpecCompletions -CurrentWord $currentWord)
                }
                if ($previousToken -eq '-recordtempto') {
                    return @(Get-WprPathCompletions -CurrentWord $currentWord -Kind 'Directory' -ToolTip 'Temporary recording folder.' -Placeholder '<temp-folder>')
                }
                if ($previousToken -eq '-shutdown' -and -not $currentWord.StartsWith('-')) {
                    return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values @('KSR') -ToolTip 'Trace for Kernel Soft Reboot (optional).')
                }
                $commonResults = @(Get-WprCommonParameterValueCompletionList -PreviousToken $previousToken -CurrentWord $currentWord)
                if ($commonResults.Count -gt 0) {
                    return $commonResults
                }
            }

            # After the first profile operand every further profile needs -start, so only options remain.
            if ((-not [string]::IsNullOrWhiteSpace($currentWord) -and $currentWord.StartsWith('-')) -or
                (Test-WprProfileOperandPresent -TokensAfterCommand $tokensAfterCommand -ValueOptions $valueOptions)) {
                return @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)
            }

            return @(Get-WprProfileSpecCompletions -CurrentWord $currentWord)
        }
        'AddBoot' {
            $valueOptions = @('-addboot', '-recordtempto', '-export')
            if ($tokensAfterCommand.Count -gt 0) {
                $previousToken = $tokensAfterCommand[-1]
                if ($previousToken -eq '-addboot') {
                    return @(Get-WprProfileSpecCompletions -CurrentWord $currentWord)
                }
                if ($previousToken -eq '-recordtempto') {
                    return @(Get-WprPathCompletions -CurrentWord $currentWord -Kind 'Directory' -ToolTip 'Temporary recording folder.' -Placeholder '<temp-folder>')
                }
                if ($previousToken -eq '-export') {
                    return @(Get-WprPathCompletions -CurrentWord $currentWord -Kind 'File' -ToolTip 'Registry export file path.' -Placeholder '<file>')
                }
            }

            if ((-not [string]::IsNullOrWhiteSpace($currentWord) -and $currentWord.StartsWith('-')) -or
                (Test-WprProfileOperandPresent -TokensAfterCommand $tokensAfterCommand -ValueOptions $valueOptions)) {
                return @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)
            }

            return @(Get-WprProfileSpecCompletions -CurrentWord $currentWord)
        }
        'Stop' {
            if ($tokensAfterCommand.Count -gt 0) {
                $commonResults = @(Get-WprCommonParameterValueCompletionList -PreviousToken $tokensAfterCommand[-1] -CurrentWord $currentWord)
                if ($commonResults.Count -gt 0) {
                    return $commonResults
                }
            }

            if ($tokensAfterCommand.Count -eq 0) {
                return @(Get-WprPathCompletions -CurrentWord $currentWord -Kind 'File' -ToolTip 'Recording output file.' -Placeholder '<recording.etl>')
            }

            if ($tokensAfterCommand.Count -eq 1 -and -not ($currentWord.StartsWith('-'))) {
                return @(Get-WprPlaceholderCompletions -CurrentWord $currentWord -Placeholder '<problem-description>' -ToolTip 'Problem description.')
            }

            return @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)
        }
        'StopBoot' {
            if ($tokensAfterCommand.Count -eq 0) {
                return @(Get-WprPathCompletions -CurrentWord $currentWord -Kind 'File' -ToolTip 'Recording output file.' -Placeholder '<recording.etl>')
            }

            return @(Get-WprPlaceholderCompletions -CurrentWord $currentWord -Placeholder '<problem-description>' -ToolTip 'Problem description.')
        }
        'Cancel' {
            if ($tokensAfterCommand.Count -gt 0) {
                $commonResults = @(Get-WprCommonParameterValueCompletionList -PreviousToken $tokensAfterCommand[-1] -CurrentWord $currentWord)
                if ($commonResults.Count -gt 0) {
                    return $commonResults
                }
            }

            return @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)
        }
        'Merge' {
            if ($tokensAfterCommand.Count -gt 0) {
                $commonResults = @(Get-WprCommonParameterValueCompletionList -PreviousToken $tokensAfterCommand[-1] -CurrentWord $currentWord)
                if ($commonResults.Count -gt 0) {
                    return $commonResults
                }
            }

            if (-not [string]::IsNullOrWhiteSpace($currentWord) -and $currentWord.StartsWith('-')) {
                return @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)
            }

            return @(Get-WprPathCompletions -CurrentWord $currentWord -Kind 'File' -ToolTip 'Trace file path.' -Placeholder '<trace.etl>')
        }
        'Status' {
            if ($tokensAfterCommand.Count -gt 0) {
                $commonResults = @(Get-WprCommonParameterValueCompletionList -PreviousToken $tokensAfterCommand[-1] -CurrentWord $currentWord)
                if ($commonResults.Count -gt 0) {
                    return $commonResults
                }
            }

            if (-not [string]::IsNullOrWhiteSpace($currentWord) -and $currentWord.StartsWith('-')) {
                return @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)
            }

            $results = New-Object System.Collections.Generic.List[object]
            foreach ($value in @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprStatusTerms) -ToolTip 'WPR status detail selector.')) {
                [void]$results.Add($value)
            }
            foreach ($value in @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)) {
                [void]$results.Add($value)
            }
            return @(Get-WprUniqueResults -Results $results.ToArray())
        }
        'Profiles' {
            return @(Get-WprPathCompletions -CurrentWord $currentWord -Kind 'File' -ToolTip 'Optional .wprp profile definition file.' -Placeholder '<profile.wprp>')
        }
        'ProfileDetails' {
            if ($tokensAfterCommand.Count -eq 0 -or ($tokensAfterCommand.Count -gt 0 -and -not ($currentWord.StartsWith('-')))) {
                return @(Get-WprPlusListCompletions -CurrentWord $currentWord)
            }

            return @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)
        }
        'ExportProfile' {
            if ($tokensAfterCommand.Count -eq 0) {
                return @(Get-WprPlusListCompletions -CurrentWord $currentWord)
            }

            if ($tokensAfterCommand.Count -eq 1 -and -not ($currentWord.StartsWith('-'))) {
                return @(Get-WprPathCompletions -CurrentWord $currentWord -Kind 'File' -ToolTip 'Destination .wprp file.' -Placeholder '<file.wprp>')
            }

            return @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)
        }
        'Providers' {
            return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprProviderSelectors) -ToolTip 'Provider selector.')
        }
        'Marker' {
            if (-not [string]::IsNullOrWhiteSpace($currentWord) -and $currentWord.StartsWith('-')) {
                return @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)
            }

            return @(Get-WprPlaceholderCompletions -CurrentWord $currentWord -Placeholder '<scenario-name>' -ToolTip 'Scenario marker name.')
        }
        'Flush' {
            if ($tokensAfterCommand.Count -gt 0) {
                $commonResults = @(Get-WprCommonParameterValueCompletionList -PreviousToken $tokensAfterCommand[-1] -CurrentWord $currentWord)
                if ($commonResults.Count -gt 0) {
                    return $commonResults
                }
            }

            return @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)
        }
        'CaptureStateOnDemand' {
            if ($tokensAfterCommand.Count -gt 0) {
                $commonResults = @(Get-WprCommonParameterValueCompletionList -PreviousToken $tokensAfterCommand[-1] -CurrentWord $currentWord)
                if ($commonResults.Count -gt 0) {
                    return $commonResults
                }
            }

            return @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)
        }
        'HeapTracingConfig' {
            if ($tokensAfterCommand.Count -eq 0) {
                return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprProcessNames) -ToolTip 'Process name.')
            }

            return @(Get-WprEnumOrPlaceholderCompletions -CurrentWord $currentWord -EnumValues (Get-WprEnableDisableValues) -Placeholder '<package-or-mode>' -ToolTip 'Enable/disable value or app package metadata.')
        }
        'SnapshotConfig' {
            if ($tokensAfterCommand.Count -eq 0) {
                return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprSnapshotOptions) -ToolTip 'Snapshot option.')
            }

            if ($tokensAfterCommand[-1] -eq '-name') {
                return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprProcessNames) -ToolTip 'Process name.')
            }

            if ($tokensAfterCommand[-1] -eq '-pid') {
                return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprProcessIds) -ToolTip 'Process ID.')
            }

            if (-not [string]::IsNullOrWhiteSpace($currentWord) -and $currentWord.StartsWith('-')) {
                return @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)
            }

            # Right after the snapshot option the required {-name | -pid} selector is still open.
            if ($tokensAfterCommand.Count -eq 1) {
                $results = New-Object System.Collections.Generic.List[object]
                foreach ($value in @(Get-WprOptionCompletions -CurrentWord $currentWord -Options $activeCommand.Options)) {
                    [void]$results.Add($value)
                }
                foreach ($value in @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprEnableDisableValues) -ToolTip 'Enable or disable snapshot capture.')) {
                    [void]$results.Add($value)
                }
                return @(Get-WprUniqueResults -Results $results.ToArray())
            }

            return @(Get-WprEnumOrPlaceholderCompletions -CurrentWord $currentWord -EnumValues (Get-WprEnableDisableValues) -Placeholder '<mode>' -ToolTip 'Enable or disable snapshot capture.')
        }
        'EnablePeriodicSnapshot' {
            if ($tokensAfterCommand.Count -eq 0) {
                return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprSnapshotOptions) -ToolTip 'Snapshot option.')
            }
            if ($tokensAfterCommand.Count -eq 1) {
                return @(Get-WprPlaceholderCompletions -CurrentWord $currentWord -Placeholder '<interval-seconds>' -ToolTip 'Periodic snapshot interval in seconds.')
            }

            return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprProcessIds) -ToolTip 'Process ID.')
        }
        'DisablePeriodicSnapshot' {
            return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprSnapshotOptions) -ToolTip 'Snapshot option.')
        }
        'SingleSnapshot' {
            if ($tokensAfterCommand.Count -eq 0) {
                return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprSnapshotOptions) -ToolTip 'Snapshot option.')
            }

            return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprProcessIds) -ToolTip 'Process ID.')
        }
        'ProfileInterval' {
            return @(Get-WprPlaceholderCompletions -CurrentWord $currentWord -Placeholder '<profiling-interval>' -ToolTip 'Sampling profile interval.')
        }
        'ResetProfileInterval' {
            return @(Get-WprPlaceholderCompletions -CurrentWord $currentWord -Placeholder '<profile-source-name>' -ToolTip 'Optional profile source name.')
        }
        'LogMode' {
            return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprLogModes) -ToolTip 'WPR debug logging mode.')
        }
        'PagingExecutive' {
            return @(Get-WprEnumCompletions -CurrentWord $currentWord -Values (Get-WprPagingModes) -ToolTip 'Disable Paging Executive mode.')
        }
        'None' {
            return @()
        }
    }

    @()
}

Register-ArgumentCompleter -Native -CommandName @('wpr', 'wpr.exe') -ScriptBlock {
    param($wordToComplete, $commandAst, $cursorPosition)

    Complete-Wpr -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition
}