completers/wslconfig_completer/wslconfig_completer.ps1

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

.DESCRIPTION
    Provides a static-first native completer for the legacy `wslconfig` and
    `wslconfig.exe` command surface.

    The completer covers:
    - top-level slash switches
    - `/list` mode options
    - local distribution-name completion for setdefault, terminate, and unregister
    - terminal handling after help switches

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


Set-StrictMode -Version Latest

function New-WslConfigCompletionResult {
    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-WslConfigTokenState {
    param(
        [string]$Line,
        [int]$CursorPosition
    )

    if ($null -eq $Line) {
        $Line = ''
    }

    $safeCursor = [Math]::Min([Math]::Max($CursorPosition, 0), $Line.Length)
    $prefix = $Line.Substring(0, $safeCursor)
    $tokens = New-Object System.Collections.Generic.List[string]
    $builder = New-Object System.Text.StringBuilder
    $quoteChar = [char]0

    foreach ($character in $prefix.ToCharArray()) {
        if (($character -eq [char]34) -or ($character -eq [char]39)) {
            if ($quoteChar -eq [char]0) {
                $quoteChar = $character
            } elseif ($quoteChar -eq $character) {
                $quoteChar = [char]0
            }

            [void]$builder.Append($character)
            continue
        }

        if ([char]::IsWhiteSpace($character) -and $quoteChar -eq [char]0) {
            if ($builder.Length -gt 0) {
                $tokens.Add($builder.ToString())
                [void]$builder.Clear()
            }

            continue
        }

        [void]$builder.Append($character)
    }

    $hasTrailingSpace = $prefix -match '\s$'
    if ($builder.Length -gt 0) {
        $tokens.Add($builder.ToString())
    }

    if ($hasTrailingSpace) {
        return [pscustomobject]@{
            TokensBeforeCurrent = @($tokens)
            CurrentToken        = ''
        }
    }

    if ($tokens.Count -gt 0) {
        return [pscustomobject]@{
            TokensBeforeCurrent = @($tokens | Select-Object -First ($tokens.Count - 1))
            CurrentToken        = $tokens[$tokens.Count - 1]
        }
    }

    [pscustomobject]@{
        TokensBeforeCurrent = @()
        CurrentToken        = ''
    }
}

function Get-WslConfigArgumentsFromTokenState {
    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)wslconfig(?:\.exe)?$') {
        $currentArgument = ''
    }

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

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

    $switches = @(
        [pscustomobject]@{ Token = '/l';          Description = 'List registered distributions.'; ValueKind = 'ListMode' }
        [pscustomobject]@{ Token = '/list';       Description = 'List registered distributions.'; ValueKind = 'ListMode' }
        [pscustomobject]@{ Token = '/all';        Description = 'List all distributions, including installing or uninstalling ones.'; ValueKind = $null }
        [pscustomobject]@{ Token = '/running';    Description = 'List only running distributions.'; ValueKind = $null }
        [pscustomobject]@{ Token = '/s';          Description = 'Set the default distribution.'; ValueKind = 'DistributionName' }
        [pscustomobject]@{ Token = '/setdefault'; Description = 'Set the default distribution.'; ValueKind = 'DistributionName' }
        [pscustomobject]@{ Token = '/t';          Description = 'Terminate the distribution.'; ValueKind = 'DistributionName' }
        [pscustomobject]@{ Token = '/terminate';  Description = 'Terminate the distribution.'; ValueKind = 'DistributionName' }
        [pscustomobject]@{ Token = '/u';          Description = 'Unregister the distribution and delete its root filesystem.'; ValueKind = 'DistributionName' }
        [pscustomobject]@{ Token = '/unregister'; Description = 'Unregister the distribution and delete its root filesystem.'; ValueKind = 'DistributionName' }
        [pscustomobject]@{ Token = '/?';          Description = 'Show help for wslconfig.'; ValueKind = $null }
        [pscustomobject]@{ Token = '-?';          Description = 'Show help for wslconfig.'; ValueKind = $null }
        [pscustomobject]@{ Token = '--help';      Description = 'Show help for wslconfig.'; ValueKind = $null }
    )

    $switchLookup = @{}
    foreach ($switch in $switches) {
        $switchLookup[$switch.Token.ToLowerInvariant()] = $switch
    }

    $script:WslConfigCompletionCatalog = [pscustomobject]@{
        Switches        = $switches
        SwitchLookup    = $switchLookup
        ListModeOptions = @('/all', '/running')
    }

    $script:WslConfigCompletionCatalog
}

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

    $values = @()
    if (Get-Command -Name wslconfig.exe -ErrorAction SilentlyContinue) {
        $values = @(
            & wslconfig.exe /l 2>$null |
                ForEach-Object { $_.ToString().Replace([string][char]0, '') } |
                ForEach-Object { $_.Trim() } |
                Where-Object {
                    $_ -and
                    $_ -notmatch '^Windows Subsystem for Linux Distributions' -and
                    $_ -notmatch '^The following distributions' -and
                    $_ -notmatch '^\*'
                } |
                ForEach-Object {
                    ($_ -replace '\s*\(Default\)\s*$', '').Trim()
                } |
                Where-Object { $_ }
        ) | Sort-Object -Unique
    }

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

    @($values)
}

function Get-WslConfigSwitchCompletions {
    param(
        [string]$CurrentWord,
        [bool]$ListMode = $false
    )

    $catalog = Get-WslConfigCatalog
    $source = if ($ListMode) {
        foreach ($token in $catalog.ListModeOptions) {
            $catalog.SwitchLookup[$token.ToLowerInvariant()]
        }
    } else {
        $catalog.Switches
    }

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

        New-WslConfigCompletionResult -CompletionText $switch.Token -ResultType 'ParameterName' -ToolTip $switch.Description
    }
}

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

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

        [void]$results.Add((New-WslConfigCompletionResult -CompletionText $distribution -ResultType 'ParameterValue' -ToolTip 'WSL distribution name.'))
    }

    if ($results.Count -eq 0) {
        $placeholder = if ([string]::IsNullOrWhiteSpace($CurrentWord)) { '<distribution-name>' } else { $CurrentWord }
        [void]$results.Add((New-WslConfigCompletionResult -CompletionText $placeholder -ResultType 'ParameterValue' -ToolTip 'WSL distribution name.'))
    }

    @($results.ToArray())
}

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

    $completionText = if ([string]::IsNullOrEmpty($CurrentWord)) { ' ' } else { $CurrentWord }
    @(
        New-WslConfigCompletionResult -CompletionText $completionText -ResultType 'ParameterValue' -ToolTip 'No further arguments are valid after help.'
    )
}

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

    $tokenState = Get-WslConfigTokenState -Line $commandAst.ToString() -CursorPosition $cursorPosition
    $argumentState = Get-WslConfigArgumentsFromTokenState -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)
    }

    $catalog = Get-WslConfigCatalog

    $helpRequested = $false
    $valueContext = $null
    $listMode = $false

    if ($argumentsBeforeCurrent.Count -gt 0) {
        foreach ($token in $argumentsBeforeCurrent) {
            $key = $token.ToLowerInvariant()
            if ($key -in @('/?', '-?', '--help')) {
                $helpRequested = $true
                break
            }
        }

        if (-not $helpRequested) {
            $lastToken = $argumentsBeforeCurrent[-1].ToLowerInvariant()
            if ($lastToken -in @('/s', '/setdefault', '/t', '/terminate', '/u', '/unregister')) {
                $valueContext = 'DistributionName'
            } elseif ($lastToken -in @('/l', '/list')) {
                $listMode = $true
            }
        }
    }

    if ($helpRequested) {
        return @(Get-WslConfigTerminalCompletions -CurrentWord $currentWord)
    }

    if ($valueContext -eq 'DistributionName') {
        return @(Get-WslConfigDistributionCompletions -CurrentWord $currentWord)
    }

    if (-not [string]::IsNullOrEmpty($currentWord) -and ($currentWord.StartsWith('/') -or $currentWord.StartsWith('-'))) {
        return @(Get-WslConfigSwitchCompletions -CurrentWord $currentWord -ListMode:$listMode)
    }

    if ($listMode) {
        return @(Get-WslConfigSwitchCompletions -CurrentWord $currentWord -ListMode $true)
    }

    if ([string]::IsNullOrWhiteSpace($currentWord)) {
        return @(Get-WslConfigSwitchCompletions -CurrentWord $currentWord)
    }

    @()
}

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

    Complete-WslConfig -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition
}