completers/compact_completer/compact_completer.ps1

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

.DESCRIPTION
    Provides a static-first native completer for `compact` and `compact.exe`.

    The completer covers:
    - slash-style switch completion
    - attached value switches such as `/S:`, `/EXE:`, `/CompactOs:`, and `/WinDir:`
    - local file and directory completion for compact operands
    - placeholder-safe enum completion for algorithm and CompactOS value slots

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


Set-StrictMode -Version 2.0

function New-CompactCompletionResult {
    param(
        [string]$CompletionText,
        [string]$ResultType,
        [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 ConvertFrom-CompactTypedWord {
    # The value of a typed word. A word holding a quote (ASCII or typographic, also mid-word) 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)
    if ($tokens[0] -is [System.Management.Automation.Language.StringToken]) {
        return $tokens[0].Value
    }

    $Value
}

function Get-CompactTypedQuote {
    # The first quote typed in a word (opening or mid-word), or '' when none was typed.
    param([string]$Value)

    $match = [regex]::Match($Value, '[''"\u2018-\u201E]')
    if ($match.Success) { $match.Value } else { '' }
}

function ConvertTo-CompactQuotedValue {
    # 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) {
        if ($Value -notmatch '[\s{}();,|&<>''"`$@#\u2018-\u201E]') {
            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-CompactArgumentState {
    param(
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition
    )

    # The parser keeps an unterminated quoted word as one element running to the cursor.
    $argumentsBeforeCurrent = New-Object System.Collections.Generic.List[string]
    $currentArgument = ''
    foreach ($element in ($CommandAst.CommandElements | Select-Object -Skip 1)) {
        $extent = $element.Extent
        if ($extent.EndOffset -lt $CursorPosition) {
            $argumentsBeforeCurrent.Add($extent.Text)
        } elseif ($extent.StartOffset -lt $CursorPosition) {
            $currentArgument = $extent.Text.Substring(0, $CursorPosition - $extent.StartOffset)
        }
    }

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

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

    $switches = @(
        [pscustomobject]@{ Token = '/C';           Description = 'Compress the specified files.' }
        [pscustomobject]@{ Token = '/U';           Description = 'Uncompress the specified files.' }
        [pscustomobject]@{ Token = '/S';           Description = 'Process the current directory and all subdirectories, or use /S:dir.' }
        [pscustomobject]@{ Token = '/S:';          Description = 'Process the specified directory and all subdirectories.'; ValueKind = 'DirectoryPath' }
        [pscustomobject]@{ Token = '/A';           Description = 'Display files with hidden or system attributes.' }
        [pscustomobject]@{ Token = '/I';           Description = 'Continue even after errors occur.' }
        [pscustomobject]@{ Token = '/F';           Description = 'Force compression of already-compressed files.' }
        [pscustomobject]@{ Token = '/Q';           Description = 'Report only essential information.' }
        [pscustomobject]@{ Token = '/EXE';         Description = 'Use executable-file compression defaults.' }
        [pscustomobject]@{ Token = '/EXE:';        Description = 'Use compression optimized for executable files.'; ValueKind = 'ExeAlgorithm' }
        [pscustomobject]@{ Token = '/CompactOs';   Description = 'Set or query the system Compact state.' }
        [pscustomobject]@{ Token = '/CompactOs:';  Description = 'Set the system Compact state.'; ValueKind = 'CompactOsOption' }
        [pscustomobject]@{ Token = '/WinDir:';     Description = 'Specify the offline Windows directory when querying CompactOS.'; ValueKind = 'DirectoryPath' }
        [pscustomobject]@{ Token = '/?';           Description = 'Show compact help.' }
    )

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

    $script:CompactCompletionCatalog = [pscustomobject]@{
        Switches         = $switches
        SwitchLookup     = $lookup
        ExeAlgorithms    = @('XPRESS4K', 'XPRESS8K', 'XPRESS16K', 'LZX')
        CompactOsOptions = @('query', 'always', 'never')
    }

    $script:CompactCompletionCatalog
}

function Get-CompactAttachedTokenInfo {
    param([string]$Token)

    if ([string]::IsNullOrWhiteSpace($Token)) {
        return $null
    }

    $match = [regex]::Match($Token, '^(?i)(?<root>/(?:S|EXE|CompactOs|WinDir)):(?<value>.*)$')
    if (-not $match.Success) {
        return $null
    }

    $catalog = Get-CompactCatalog
    $switchKey = ($match.Groups['root'].Value + ':').ToLowerInvariant()
    if (-not $catalog.SwitchLookup.ContainsKey($switchKey)) {
        return $null
    }

    $switch = $catalog.SwitchLookup[$switchKey]
    [pscustomobject]@{
        Prefix = $switch.Token
        Value  = $match.Groups['value'].Value
        Switch = $switch
    }
}

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

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

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

function Get-CompactPrefixedValueCompletions {
    param(
        [string]$Prefix,
        [string]$CurrentValue,
        [string[]]$Suggestions,
        [string]$ToolTip,
        [string]$Placeholder
    )

    $typedValue = ConvertFrom-CompactTypedWord -Value $CurrentValue
    $quoteChar = Get-CompactTypedQuote -Value $CurrentValue
    $results = New-Object System.Collections.Generic.List[object]

    foreach ($suggestion in $Suggestions) {
        if (-not [string]::IsNullOrWhiteSpace($typedValue) -and
            -not $suggestion.StartsWith($typedValue, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        [void]$results.Add((New-CompactCompletionResult -CompletionText ($Prefix + $suggestion) -ResultType 'ParameterValue' -ToolTip $ToolTip -ListItemText ($Prefix + $suggestion)))
    }

    if ($results.Count -eq 0) {
        $fallback = if ([string]::IsNullOrWhiteSpace($CurrentValue)) { $Prefix + $Placeholder } elseif (-not $quoteChar) { $Prefix + $CurrentValue } else { $Prefix + (ConvertTo-CompactQuotedValue -Value $typedValue -QuoteChar $quoteChar) }
        [void]$results.Add((New-CompactCompletionResult -CompletionText $fallback -ResultType 'ParameterValue' -ToolTip $ToolTip -ListItemText $fallback))
    }

    @($results.ToArray())
}

function Get-CompactPathCompletions {
    param(
        [string]$CurrentValue,
        [string]$Prefix,
        [ValidateSet('File','Directory','Any')]
        [string]$Kind,
        [string]$ToolTip,
        [string]$Placeholder,
        [switch]$NoPlaceholder
    )

    $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue }
    $cleanValue = ConvertFrom-CompactTypedWord -Value $typedValue
    $quoteChar = Get-CompactTypedQuote -Value $typedValue
    $results = New-Object System.Collections.Generic.List[object]

    # The typed directory part (.\, ../, sub/, C:) is kept exactly as typed on every candidate.
    $directoryPart = ''
    $separatorIndex = $cleanValue.LastIndexOfAny([char[]]@('\', '/'))
    if ($separatorIndex -ge 0) {
        $directoryPart = $cleanValue.Substring(0, $separatorIndex + 1)
    } elseif ($cleanValue -match '^[A-Za-z]:') {
        $directoryPart = $cleanValue.Substring(0, 2)
    }

    $leaf = $cleanValue.Substring($directoryPart.Length)
    $parentPath = if ($directoryPart) { $directoryPart } else { '.' }
    $separator = if ($directoryPart.EndsWith('/')) { '/' } else { [System.IO.Path]::DirectorySeparatorChar }

    $items = @()
    if (Test-Path -LiteralPath $parentPath -PathType Container -ErrorAction Ignore) {
        $items = @(Get-ChildItem -LiteralPath $parentPath -Force -ErrorAction Ignore)
    }

    foreach ($item in $items) {
        if ($Kind -eq 'Directory' -and -not $item.PSIsContainer) {
            continue
        }

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

        $candidate = $directoryPart + $item.Name
        # A whole word starting with a dash would parse as a parameter: prefix .\ as PowerShell does.
        if (-not $Prefix -and -not $directoryPart -and $candidate -match '^[-\u2013-\u2015]') {
            $candidate = '.' + $separator + $candidate
        }

        if ($item.PSIsContainer) {
            $candidate += $separator
        }

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

    if ($results.Count -eq 0 -and -not $NoPlaceholder) {
        $fallback = if ([string]::IsNullOrWhiteSpace($typedValue)) { $Prefix + $Placeholder } elseif (-not $quoteChar) { $Prefix + $typedValue } else { $Prefix + (ConvertTo-CompactQuotedValue -Value $cleanValue -QuoteChar $quoteChar) }
        [void]$results.Add((New-CompactCompletionResult -CompletionText $fallback -ResultType 'ParameterValue' -ToolTip $ToolTip -ListItemText $fallback))
    }

    @($results.ToArray())
}

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

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

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

    $argumentState = Get-CompactArgumentState -CommandAst $commandAst -CursorPosition $cursorPosition
    $currentWord = $argumentState.CurrentArgument
    $argumentsBeforeCurrent = @($argumentState.ArgumentsBeforeCurrent)

    $helpRequested = $argumentsBeforeCurrent -contains '/?'
    $catalog = Get-CompactCatalog

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

    if (-not [string]::IsNullOrWhiteSpace($currentWord) -and $currentWord.StartsWith('/')) {
        $attached = Get-CompactAttachedTokenInfo -Token $currentWord
        if ($null -ne $attached) {
            switch ($attached.Switch.ValueKind) {
                'ExeAlgorithm' {
                    return @(Get-CompactPrefixedValueCompletions -Prefix $attached.Prefix -CurrentValue $attached.Value -Suggestions $catalog.ExeAlgorithms -ToolTip $attached.Switch.Description -Placeholder '<algorithm>')
                }
                'CompactOsOption' {
                    return @(Get-CompactPrefixedValueCompletions -Prefix $attached.Prefix -CurrentValue $attached.Value -Suggestions $catalog.CompactOsOptions -ToolTip $attached.Switch.Description -Placeholder '<option>')
                }
                'DirectoryPath' {
                    return @(Get-CompactPathCompletions -CurrentValue $attached.Value -Prefix $attached.Prefix -Kind 'Directory' -ToolTip $attached.Switch.Description -Placeholder '<dir>')
                }
            }
        }

        return @(Get-CompactSwitchCompletions -CurrentWord $currentWord)
    }

    if ([string]::IsNullOrWhiteSpace($currentWord)) {
        return @(
            Get-CompactSwitchCompletions -CurrentWord $currentWord
            Get-CompactPathCompletions -CurrentValue '' -Prefix '' -Kind 'Any' -ToolTip 'File or directory pattern.' -Placeholder '<path>' -NoPlaceholder
        )
    }

    @(Get-CompactPathCompletions -CurrentValue $currentWord -Prefix '' -Kind 'Any' -ToolTip 'File or directory pattern.' -Placeholder '<path>')
}

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

    Complete-Compact -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition
}