completers/rustc_completer/rustc_completer.ps1

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

.DESCRIPTION
    Provides a help-driven native completer for `rustc` and `rustc.exe`.

    The completer keeps its top level compatible with `Import-CompleterScript` by
    limiting top-level content to `Set-StrictMode`, function definitions, and one
    literal `Register-ArgumentCompleter -Native` call.
#>


Set-StrictMode -Version 2.0

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

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

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

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

function Get-RustcTokenState {
    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) {
                [void]$tokens.Add($builder.ToString())
                [void]$builder.Clear()
            }

            continue
        }

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

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

    if ($hasTrailingSpace) {
        return [pscustomobject]@{
            TokensBeforeCurrent = @($tokens.ToArray())
            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 Remove-RustcOuterQuotes {
    param([string]$Value)

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

    if ($Value.Length -ge 2) {
        if ($Value.StartsWith('"') -and $Value.EndsWith('"')) {
            return $Value.Substring(1, $Value.Length - 2).Replace('`"', '"')
        }

        if ($Value.StartsWith("'") -and $Value.EndsWith("'")) {
            return $Value.Substring(1, $Value.Length - 2).Replace("''", "'")
        }
    }

    $Value.TrimStart('"', "'")
}

function ConvertTo-RustcQuotedValue {
    param(
        [string]$Value,
        [string]$OriginalToken = ''
    )

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

    $needsQuote = $Value -match '\s'
    $preferSingle = $OriginalToken.StartsWith("'")

    if (-not $needsQuote -and -not $OriginalToken.StartsWith('"') -and -not $preferSingle) {
        return $Value
    }

    if ($preferSingle -and -not $Value.Contains("'")) {
        return "'" + $Value + "'"
    }

    '"' + $Value.Replace('`', '``').Replace('"', '`"') + '"'
}

function Split-RustcPathToken {
    param([string]$Value)

    # Split on the last separator instead of Split-Path: Split-Path throws on an
    # empty string, and on a trailing separator it returns the directory itself
    # as the leaf, so a path slot could never descend.
    if ([string]::IsNullOrEmpty($Value)) {
        return [pscustomobject]@{ Prefix = ''; Parent = '.'; Leaf = '' }
    }

    if ($Value -match '^[A-Za-z]:$') {
        return [pscustomobject]@{ Prefix = $Value; Parent = ($Value + [IO.Path]::DirectorySeparatorChar); Leaf = '' }
    }

    $index = $Value.LastIndexOfAny([char[]]@('\', '/'))
    if ($index -lt 0) {
        return [pscustomobject]@{ Prefix = ''; Parent = '.'; Leaf = $Value }
    }

    $prefix = $Value.Substring(0, $index + 1)
    [pscustomobject]@{ Prefix = $prefix; Parent = $prefix; Leaf = $Value.Substring($index + 1) }
}

function Get-RustcPathCompletions {
    param(
        [string]$CurrentToken,
        [bool]$DirectoriesOnly = $false,
        [bool]$FilesOnly = $false,
        [bool]$RustSourceOnly = $false
    )

    $raw = if ($null -eq $CurrentToken) { '' } else { $CurrentToken }
    $clean = Remove-RustcOuterQuotes -Value $raw
    $split = Split-RustcPathToken -Value $clean
    $leaf = $split.Leaf

    $results = New-Object System.Collections.Generic.List[System.Management.Automation.CompletionResult]
    try {
        $items = Get-ChildItem -LiteralPath $split.Parent -Force -ErrorAction Stop
    } catch {
        return @()
    }

    foreach ($item in $items) {
        if ($DirectoriesOnly -and -not $item.PSIsContainer) {
            continue
        }

        if ($FilesOnly -and $item.PSIsContainer) {
            continue
        }

        if ($RustSourceOnly -and -not $item.PSIsContainer -and $item.Extension -ne '.rs') {
            continue
        }

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

        $completionPath = $split.Prefix + $item.Name
        if ($item.PSIsContainer) {
            $completionPath += [IO.Path]::DirectorySeparatorChar
        }

        $quoted = ConvertTo-RustcQuotedValue -Value $completionPath -OriginalToken $raw
        [void]$results.Add((New-RustcCompletionResult -CompletionText $quoted -ResultType 'ParameterValue' -ToolTip $item.FullName -ListItemText $item.Name))
    }

    @($results.ToArray() | Sort-Object ListItemText)
}

function Get-RustcCommandPath {
    if (Test-Path -LiteralPath variable:script:RustcCommandPath) {
        return $script:RustcCommandPath
    }

    $command = Get-Command -Name rustc.exe, rustc -ErrorAction SilentlyContinue | Select-Object -First 1
    $script:RustcCommandPath = if ($command) {
        if ($command.Source) { $command.Source } else { $command.Name }
    } else {
        $null
    }

    $script:RustcCommandPath
}

function Invoke-RustcText {
    param([string[]]$Arguments)

    $commandPath = Get-RustcCommandPath
    if (-not $commandPath) {
        return @()
    }

    try {
        @(& $commandPath @Arguments 2>$null | ForEach-Object { $_ -replace '\e\[[0-9;?]*[ -/]*[@-~]', '' })
    } catch {
        @()
    }
}

function Get-RustcCatalog {
    if (Test-Path -LiteralPath variable:script:RustcCompletionCatalog) {
        return $script:RustcCompletionCatalog
    }

    $switches = @(
        [pscustomobject]@{ Token = '-h'; Aliases = @('-h', '--help'); Description = 'Display this message'; ValueKind = $null }
        [pscustomobject]@{ Token = '--cfg'; Aliases = @('--cfg'); Description = 'Configure the compilation environment.'; ValueKind = 'CfgSpec' }
        [pscustomobject]@{ Token = '--check-cfg'; Aliases = @('--check-cfg'); Description = 'Provide list of expected cfgs for checking.'; ValueKind = 'CheckCfgSpec' }
        [pscustomobject]@{ Token = '-L'; Aliases = @('-L'); Description = 'Add a directory to the library search path.'; ValueKind = 'LibrarySearchPath' }
        [pscustomobject]@{ Token = '-l'; Aliases = @('-l'); Description = 'Link generated crates to the specified native library.'; ValueKind = 'LinkLibrary' }
        [pscustomobject]@{ Token = '--crate-type'; Aliases = @('--crate-type'); Description = 'Specify emitted crate types.'; ValueKind = 'CrateType' }
        [pscustomobject]@{ Token = '--crate-name'; Aliases = @('--crate-name'); Description = 'Specify the crate name.'; ValueKind = 'CrateName' }
        [pscustomobject]@{ Token = '--edition'; Aliases = @('--edition'); Description = 'Specify the Rust edition.'; ValueKind = 'Edition' }
        [pscustomobject]@{ Token = '--emit'; Aliases = @('--emit'); Description = 'Specify emitted output types.'; ValueKind = 'EmitType' }
        [pscustomobject]@{ Token = '--print'; Aliases = @('--print'); Description = 'Print compiler information.'; ValueKind = 'PrintInfo' }
        [pscustomobject]@{ Token = '-g'; Aliases = @('-g'); Description = 'Equivalent to -C debuginfo=2.'; ValueKind = $null }
        [pscustomobject]@{ Token = '-O'; Aliases = @('-O'); Description = 'Equivalent to -C opt-level=3.'; ValueKind = $null }
        [pscustomobject]@{ Token = '-o'; Aliases = @('-o'); Description = 'Write output to file.'; ValueKind = 'OutputFile' }
        [pscustomobject]@{ Token = '--out-dir'; Aliases = @('--out-dir'); Description = 'Write output to directory.'; ValueKind = 'OutputDir' }
        [pscustomobject]@{ Token = '--explain'; Aliases = @('--explain'); Description = 'Explain an error code.'; ValueKind = 'ExplainCode' }
        [pscustomobject]@{ Token = '--test'; Aliases = @('--test'); Description = 'Build a test harness.'; ValueKind = $null }
        [pscustomobject]@{ Token = '--target'; Aliases = @('--target'); Description = 'Compile for a target tuple.'; ValueKind = 'TargetTriple' }
        [pscustomobject]@{ Token = '-A'; Aliases = @('-A', '--allow'); Description = 'Set lint allowed.'; ValueKind = 'LintName' }
        [pscustomobject]@{ Token = '-W'; Aliases = @('-W', '--warn'); Description = 'Set lint warnings.'; ValueKind = 'LintName' }
        [pscustomobject]@{ Token = '--force-warn'; Aliases = @('--force-warn'); Description = 'Set lint force-warn.'; ValueKind = 'LintName' }
        [pscustomobject]@{ Token = '-D'; Aliases = @('-D', '--deny'); Description = 'Set lint denied.'; ValueKind = 'LintName' }
        [pscustomobject]@{ Token = '-F'; Aliases = @('-F', '--forbid'); Description = 'Set lint forbidden.'; ValueKind = 'LintName' }
        [pscustomobject]@{ Token = '--cap-lints'; Aliases = @('--cap-lints'); Description = 'Cap lint level.'; ValueKind = 'LintLevel' }
        [pscustomobject]@{ Token = '-C'; Aliases = @('-C', '--codegen'); Description = 'Set a codegen option.'; ValueKind = 'CodegenOption' }
        [pscustomobject]@{ Token = '-V'; Aliases = @('-V', '--version'); Description = 'Print version info and exit.'; ValueKind = $null }
        [pscustomobject]@{ Token = '-v'; Aliases = @('-v', '--verbose'); Description = 'Use verbose output.'; ValueKind = $null }
        # Documented by `rustc --help -v` only, but accepted on every invocation.
        [pscustomobject]@{ Token = '--extern'; Aliases = @('--extern'); Description = 'Specify where an external rust library is located.'; ValueKind = 'ExternSpec' }
        [pscustomobject]@{ Token = '--sysroot'; Aliases = @('--sysroot'); Description = 'Override the system root.'; ValueKind = 'OutputDir' }
        [pscustomobject]@{ Token = '--error-format'; Aliases = @('--error-format'); Description = 'How errors and other messages are produced.'; ValueKind = 'ErrorFormat' }
        [pscustomobject]@{ Token = '--json'; Aliases = @('--json'); Description = 'Configure the JSON output of the compiler.'; ValueKind = 'JsonConfig' }
        [pscustomobject]@{ Token = '--color'; Aliases = @('--color'); Description = 'Configure coloring of output.'; ValueKind = 'ColorMode' }
        [pscustomobject]@{ Token = '--diagnostic-width'; Aliases = @('--diagnostic-width'); Description = 'Inform rustc of the width of the output.'; ValueKind = 'DiagnosticWidth' }
        [pscustomobject]@{ Token = '--remap-path-prefix'; Aliases = @('--remap-path-prefix'); Description = 'Remap source names in all output.'; ValueKind = 'RemapPathPrefix' }
        [pscustomobject]@{ Token = '--remap-path-scope'; Aliases = @('--remap-path-scope'); Description = 'Which scopes of paths --remap-path-prefix remaps.'; ValueKind = 'RemapPathScope' }
    )

    $aliasLookup = New-Object 'System.Collections.Generic.Dictionary[string, object]' ([System.StringComparer]::Ordinal)
    foreach ($switch in $switches) {
        foreach ($alias in $switch.Aliases) {
            $aliasLookup[$alias] = $switch
        }
    }

    $topHelp = Invoke-RustcText -Arguments @('--help', '-v')
    foreach ($line in $topHelp) {
        if ($line -match '^\s+(-\w(?:,\s+--[A-Za-z0-9\-]+)?|--[A-Za-z0-9\-]+)\b') {
            $tokenGroup = $matches[1]
            foreach ($token in ($tokenGroup -split ',\s*')) {
                $key = $token
                if ($aliasLookup.ContainsKey($key)) {
                    $spec = $aliasLookup[$key]
                    if ($line -match '\s{2,}(.*)$') {
                        $spec.Description = $matches[1].Trim()
                    }
                }
            }
        }
    }

    # `rustc -W help` prints two tables: "Lint checks provided by rustc:" with a
    # default level per lint, and "Lint groups provided by rustc:" with the
    # group's sub-lints. Both are valid -A/-W/-D/-F/--force-warn values.
    $lintNames = New-Object System.Collections.Generic.List[string]
    $lintGroups = New-Object System.Collections.Generic.List[object]
    $inGroups = $false
    foreach ($line in (Invoke-RustcText -Arguments @('-W', 'help'))) {
        if ($line -match '^Lint groups provided by') {
            $inGroups = $true
            continue
        }

        if ($line -match '^[A-Za-z].*:\s*$') {
            $inGroups = $false
            continue
        }

        if ($line -match '^\s{2,}([a-z0-9][a-z0-9\-]*)\s{2,}(allow|warn|deny|forbid)\s{2,}') {
            [void]$lintNames.Add($matches[1])
            continue
        }

        if ($inGroups -and $line -match '^\s{2,}(?<name>[a-z0-9][a-z0-9\-]*)\s{2,}(?<sub>\S.*)$' -and $matches.name -ne 'name') {
            [void]$lintGroups.Add([pscustomobject]@{ Name = $matches.name; SubLints = $matches.sub.Trim() })
        }
    }

    $codegenOptions = New-Object System.Collections.Generic.List[string]
    foreach ($line in (Invoke-RustcText -Arguments @('-C', 'help'))) {
        if ($line -match '^\s+-C\s+([A-Za-z0-9\-]+)=val\b') {
            [void]$codegenOptions.Add($matches[1])
        }
    }

    $targetTriples = Invoke-RustcText -Arguments @('--print', 'target-list')
    $targetCpus = New-Object System.Collections.Generic.List[string]
    foreach ($line in (Invoke-RustcText -Arguments @('--print', 'target-cpus'))) {
        if ($line -match '^\s{4}([A-Za-z0-9_\-+.]+)\b') {
            [void]$targetCpus.Add($matches[1])
        }
    }

    $script:RustcCompletionCatalog = [pscustomobject]@{
        Switches       = $switches
        AliasLookup    = $aliasLookup
        Editions       = @('2015', '2018', '2021', '2024', 'future')
        CrateTypes     = @('bin', 'lib', 'rlib', 'dylib', 'cdylib', 'staticlib', 'proc-macro')
        EmitTypes      = @('asm', 'llvm-bc', 'dep-info', 'link', 'llvm-ir', 'metadata', 'mir', 'obj', 'thin-link-bitcode')
        PrintInfos     = @('all-target-specs-json', 'backend-has-zstd', 'calling-conventions', 'cfg', 'check-cfg', 'code-models', 'crate-name', 'crate-root-lint-levels', 'deployment-target', 'file-names', 'host-tuple', 'link-args', 'native-static-libs', 'relocation-models', 'split-debuginfo', 'stack-protector-strategies', 'supported-crate-types', 'sysroot', 'target-cpus', 'target-features', 'target-libdir', 'target-list', 'target-spec-json', 'target-spec-json-schema', 'tls-models')
        LintLevels     = @('allow', 'warn', 'deny', 'forbid')
        LintNames      = @($lintNames.ToArray() | Sort-Object -Unique)
        LintGroups     = @($lintGroups.ToArray() | Sort-Object -Property Name -Unique)
        CodegenOptions = @($codegenOptions.ToArray() | Sort-Object -Unique)
        TargetTriples  = @($targetTriples | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
        TargetCpus     = @($targetCpus.ToArray() | Sort-Object -Unique)
        TargetFeatures = @('help')
        CodeModels     = @('tiny', 'small', 'kernel', 'medium', 'large')
        LtoModes       = @('off', 'thin', 'fat', 'yes', 'no')
        PanicModes     = @('abort', 'unwind')
        StripModes     = @('none', 'debuginfo', 'symbols')
        SplitDebuginfo = @('off', 'packed', 'unpacked')
        SymbolMangling = @('legacy', 'v0', 'hashed')
        OptLevels      = @('0', '1', '2', '3', 's', 'z')
        BoolValues     = @('yes', 'no', 'on', 'off', 'true', 'false')
    }

    $script:RustcCompletionCatalog
}

function Get-RustcValueKindSuggestions {
    param(
        [string]$ValueKind,
        [string]$CurrentToken
    )

    $catalog = Get-RustcCatalog
    $raw = if ($null -eq $CurrentToken) { '' } else { $CurrentToken }
    $clean = Remove-RustcOuterQuotes -Value $raw
    $results = New-Object System.Collections.Generic.List[System.Management.Automation.CompletionResult]

    switch ($ValueKind) {
        'Edition' {
            foreach ($value in $catalog.Editions) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'Rust edition'))
                }
            }
        }
        'CrateType' {
            foreach ($value in $catalog.CrateTypes) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'rustc crate type'))
                }
            }
        }
        'EmitType' {
            foreach ($value in $catalog.EmitTypes) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'rustc emit type'))
                }
            }
            if ($clean -and ($results.Count -eq 0)) {
                [void]$results.Add((New-RustcCompletionResult -CompletionText $clean -ResultType 'ParameterValue' -ToolTip 'Custom --emit value'))
            }
        }
        'PrintInfo' {
            foreach ($value in $catalog.PrintInfos) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'rustc --print topic'))
                }
            }
        }
        'TargetTriple' {
            foreach ($value in $catalog.TargetTriples) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'Rust target triple'))
                }
            }
        }
        'LintName' {
            foreach ($group in $catalog.LintGroups) {
                if ($group.Name.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $group.Name -ResultType 'ParameterValue' -ToolTip "lint group: $($group.SubLints)"))
                }
            }
            foreach ($value in $catalog.LintNames) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'rustc lint name'))
                }
            }
        }
        'ErrorFormat' {
            foreach ($value in @('human', 'json', 'short')) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'rustc --error-format'))
                }
            }
        }
        'ColorMode' {
            foreach ($value in @('auto', 'always', 'never')) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'rustc --color'))
                }
            }
        }
        'RemapPathScope' {
            foreach ($value in @('macro', 'diagnostics', 'debuginfo', 'coverage', 'object', 'all')) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'rustc --remap-path-scope'))
                }
            }
        }
        'JsonConfig' {
            foreach ($value in @('artifacts', 'diagnostic-short', 'diagnostic-rendered-ansi', 'diagnostic-unicode', 'future-incompat')) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'rustc --json config'))
                }
            }
        }
        'DiagnosticWidth' {
            if ([string]::IsNullOrEmpty($clean)) {
                [void]$results.Add((New-RustcCompletionResult -CompletionText '<width>' -ResultType 'ParameterValue' -ToolTip 'Output width in columns'))
            }
        }
        'RemapPathPrefix' {
            if ([string]::IsNullOrEmpty($clean)) {
                [void]$results.Add((New-RustcCompletionResult -CompletionText '<from>=<to>' -ResultType 'ParameterValue' -ToolTip 'rustc --remap-path-prefix'))
            }
        }
        'ExternSpec' {
            if ([string]::IsNullOrEmpty($clean)) {
                [void]$results.Add((New-RustcCompletionResult -CompletionText '<name>=<path>' -ResultType 'ParameterValue' -ToolTip 'rustc --extern NAME[=PATH]'))
            }
        }
        'LintLevel' {
            foreach ($value in $catalog.LintLevels) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'Lint level'))
                }
            }
        }
        'CodegenOption' {
            if ($clean -match '^([^=]+)=(.*)$') {
                $optionName = $matches[1]
                $optionValuePrefix = $matches[2]
                foreach ($value in (Get-RustcCodegenValueSuggestions -OptionName $optionName -ValuePrefix $optionValuePrefix)) {
                    [void]$results.Add($value)
                }
            } else {
                foreach ($value in $catalog.CodegenOptions) {
                    if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                        [void]$results.Add((New-RustcCompletionResult -CompletionText ($value + '=') -ResultType 'ParameterValue' -ToolTip 'rustc -C option'))
                    }
                }
            }
        }
        'LibrarySearchPath' { return Get-RustcPathCompletions -CurrentToken $raw -DirectoriesOnly $true }
        'OutputDir' { return Get-RustcPathCompletions -CurrentToken $raw -DirectoriesOnly $true }
        'OutputFile' { return Get-RustcPathCompletions -CurrentToken $raw }
        'InputFile' { return Get-RustcPathCompletions -CurrentToken $raw -RustSourceOnly $true }
        'ExplainCode' {
            foreach ($value in @('E0001', 'E0308', 'E0425', 'E0599', '<error-code>')) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'rustc --explain code'))
                }
            }
        }
        'CfgSpec' {
            foreach ($value in @('feature="<name>"', 'test', 'debug_assertions', '<name>[="<value>"]')) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'cfg specification'))
                }
            }
        }
        'CheckCfgSpec' {
            foreach ($value in @('cfg(<name>)', 'cfg(<name>, values("<value>"))', '<spec>')) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'check-cfg specification'))
                }
            }
        }
        'LinkLibrary' {
            foreach ($value in @('<name>', 'static=<name>', 'dylib=<name>', 'framework=<name>', 'static:+bundle=<name>')) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'Native library specification'))
                }
            }
        }
        'CrateName' {
            foreach ($value in @('<name>', 'my_crate')) {
                if ($value.StartsWith($clean, [System.StringComparison]::OrdinalIgnoreCase)) {
                    [void]$results.Add((New-RustcCompletionResult -CompletionText $value -ResultType 'ParameterValue' -ToolTip 'Crate name'))
                }
            }
        }
    }

    @($results.ToArray())
}

function Get-RustcCodegenValueSuggestions {
    param(
        [string]$OptionName,
        [string]$ValuePrefix
    )

    $catalog = Get-RustcCatalog
    $results = New-Object System.Collections.Generic.List[System.Management.Automation.CompletionResult]
    $name = $OptionName.ToLowerInvariant()

    $values = switch ($name) {
        'opt-level' { $catalog.OptLevels }
        'debug-assertions' { $catalog.BoolValues }
        'embed-bitcode' { $catalog.BoolValues }
        'force-frame-pointers' { $catalog.BoolValues }
        'force-unwind-tables' { $catalog.BoolValues }
        'link-dead-code' { $catalog.BoolValues }
        'prefer-dynamic' { $catalog.BoolValues }
        'rpath' { $catalog.BoolValues }
        'save-temps' { $catalog.BoolValues }
        'target-cpu' { $catalog.TargetCpus }
        'target-feature' { $catalog.TargetFeatures }
        'code-model' { $catalog.CodeModels }
        'lto' { $catalog.LtoModes }
        'panic' { $catalog.PanicModes }
        'strip' { $catalog.StripModes }
        'split-debuginfo' { $catalog.SplitDebuginfo }
        'symbol-mangling-version' { $catalog.SymbolMangling }
        default { @() }
    }

    foreach ($value in $values) {
        if ($value.StartsWith($ValuePrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
            [void]$results.Add((New-RustcCompletionResult -CompletionText ($OptionName + '=' + $value) -ResultType 'ParameterValue' -ToolTip "rustc -C $OptionName value"))
        }
    }

    if (($results.Count -eq 0) -and -not [string]::IsNullOrWhiteSpace($OptionName)) {
        [void]$results.Add((New-RustcCompletionResult -CompletionText ($OptionName + '=<value>') -ResultType 'ParameterValue' -ToolTip 'Codegen option value placeholder'))
    }

    @($results.ToArray())
}

function Get-RustcState {
    param([string[]]$TokensBeforeCurrent)

    $catalog = Get-RustcCatalog
    $pendingValueKind = $null
    $operandCount = 0

    foreach ($token in $TokensBeforeCurrent) {
        $clean = Remove-RustcOuterQuotes -Value $token
        if ([string]::IsNullOrWhiteSpace($clean)) {
            continue
        }

        if ($pendingValueKind) {
            $pendingValueKind = $null
            continue
        }

        if ($clean -match '^(--[A-Za-z0-9\-]+)=(.*)$') {
            continue
        }

        $lookup = $clean
        if ($catalog.AliasLookup.ContainsKey($lookup)) {
            $spec = $catalog.AliasLookup[$lookup]
            if ($spec.ValueKind) {
                $pendingValueKind = $spec.ValueKind
            }
            continue
        }

        if ($clean.StartsWith('-')) {
            continue
        }

        $operandCount++
    }

    [pscustomobject]@{
        PendingValueKind = $pendingValueKind
        OperandCount     = $operandCount
    }
}

function Get-RustcSwitchSuggestions {
    param([string]$CurrentToken)

    $catalog = Get-RustcCatalog
    $clean = Remove-RustcOuterQuotes -Value $CurrentToken
    $results = New-Object System.Collections.Generic.List[System.Management.Automation.CompletionResult]

    # Ordinal throughout: rustc's short flags are case-distinct in three pairs
    # (-L/-l, -O/-o, -V/-v). A case-insensitive match plus a case-insensitive
    # Sort-Object -Unique keeps only one spelling of each pair.
    $seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal)
    foreach ($switch in $catalog.Switches) {
        foreach ($alias in $switch.Aliases) {
            if ($alias.StartsWith($clean, [System.StringComparison]::Ordinal) -and $seen.Add($alias)) {
                [void]$results.Add((New-RustcCompletionResult -CompletionText $alias -ResultType 'ParameterName' -ToolTip $switch.Description -ListItemText $alias))
            }
        }
    }

    @($results.ToArray() | Sort-Object -Property CompletionText -CaseSensitive)
}

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

    # $cursorPosition is an offset into the whole input line, while
    # $commandAst.Extent.Text is command-relative.
    $relativeCursor = $cursorPosition - $commandAst.Extent.StartOffset
    $tokenState = Get-RustcTokenState -Line $commandAst.Extent.Text -CursorPosition $relativeCursor
    $currentToken = if ($null -eq $tokenState.CurrentToken) { $wordToComplete } else { $tokenState.CurrentToken }
    $tokensBeforeCurrent = @($tokenState.TokensBeforeCurrent)
    if ([string]::IsNullOrEmpty($wordToComplete) -and -not [string]::IsNullOrEmpty($currentToken)) {
        $tokensBeforeCurrent = @($tokensBeforeCurrent + $currentToken)
        $currentToken = ''
    }
    $tokensBeforeCurrent = @($tokensBeforeCurrent | Select-Object -Skip 1)
    $state = Get-RustcState -TokensBeforeCurrent $tokensBeforeCurrent

    if ($state.PendingValueKind) {
        return Get-RustcValueKindSuggestions -ValueKind $state.PendingValueKind -CurrentToken $currentToken
    }

    $cleanCurrent = Remove-RustcOuterQuotes -Value $currentToken
    if ($cleanCurrent -match '^(--[A-Za-z0-9\-]+)=(.*)$') {
        $catalog = Get-RustcCatalog
        $optionName = $matches[1]
        $attachedValue = $matches[2]
        if ($catalog.AliasLookup.ContainsKey($optionName)) {
            $spec = $catalog.AliasLookup[$optionName]
            if ($spec.ValueKind) {
                # Keep the "--opt=" prefix on every suggestion so accepting one
                # does not delete the option name.
                return @(
                    Get-RustcValueKindSuggestions -ValueKind $spec.ValueKind -CurrentToken $attachedValue |
                        ForEach-Object {
                            New-RustcCompletionResult -CompletionText ($optionName + '=' + $_.CompletionText) -ResultType $_.ResultType -ToolTip $_.ToolTip -ListItemText $_.ListItemText
                        }
                )
            }
        }

        return @()
    }

    if ($cleanCurrent.StartsWith('-')) {
        return Get-RustcSwitchSuggestions -CurrentToken $currentToken
    }

    if ([string]::IsNullOrEmpty($cleanCurrent)) {
        # The INPUT operand is reachable from a bare TAB: offer the switches and
        # the .rs sources rustc actually compiles.
        return @(
            @(Get-RustcSwitchSuggestions -CurrentToken $currentToken) +
            @(Get-RustcPathCompletions -CurrentToken $currentToken -RustSourceOnly $true)
        )
    }

    Get-RustcPathCompletions -CurrentToken $currentToken -RustSourceOnly $true
}