completers/uv_completer/uv_completer.ps1

Set-StrictMode -Version Latest

if (-not (Get-Variable -Name UvCompletionCache -Scope Script -ErrorAction Ignore)) {
    $script:UvCompletionCache = @{
        ExecutablePaths = @{}
        ProbedPaths     = @{}
        PathData        = @{}
        StaticTree     = @{
            ''          = @('auth', 'run', 'init', 'add', 'remove', 'version', 'sync', 'lock', 'export', 'tree', 'format', 'check', 'audit', 'tool', 'python', 'pip', 'venv', 'build', 'publish', 'workspace', 'cache', 'self', 'help')
            'auth'      = @('login', 'logout', 'token', 'dir')
            'tool'      = @('run', 'install', 'upgrade', 'list', 'audit', 'uninstall', 'update-shell', 'dir')
            'python'    = @('list', 'install', 'upgrade', 'find', 'pin', 'dir', 'uninstall', 'update-shell')
            'pip'       = @('compile', 'sync', 'install', 'uninstall', 'freeze', 'list', 'show', 'tree', 'check')
            'workspace' = @('metadata', 'dir', 'list')
            'cache'     = @('clean', 'prune', 'dir', 'size')
            'self'      = @('update', 'version')
        }
    }
}

function Get-UvCacheKey {
    param([string[]]$Path)

    $pathItems = @($Path)
    if ($null -eq $Path) {
        $pathItems = @()
    }
    $pathItems = @($pathItems | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })

    ($pathItems | ForEach-Object { $_.ToLowerInvariant() }) -join ' '
}

function Get-UvPathDataCacheKey {
    param(
        [string]$SourceName = 'uv',
        [string[]]$Path
    )

    $sourceKey = if ([string]::IsNullOrWhiteSpace($SourceName)) {
        'uv'
    } else {
        $SourceName.ToLowerInvariant()
    }

    $pathKey = Get-UvCacheKey -Path $Path
    if ([string]::IsNullOrWhiteSpace($pathKey)) {
        return $sourceKey
    }

    '{0}|{1}' -f $sourceKey, $pathKey
}

function Get-UvSourceName {
    param([string]$CommandName)

    if ([string]::IsNullOrWhiteSpace($CommandName)) {
        return 'uv'
    }

    $leafName = Split-Path -Leaf $CommandName
    if ($leafName.Equals('uvx', [System.StringComparison]::OrdinalIgnoreCase) -or
        $leafName.Equals('uvx.exe', [System.StringComparison]::OrdinalIgnoreCase)) {
        return 'uvx'
    }

    'uv'
}

function Get-UvSyntheticRootPath {
    param([string]$SourceName)

    if ($SourceName -eq 'uvx') {
        return @('tool', 'run')
    }

    @()
}

function Get-UvExecutablePath {
    param([string]$CommandName = 'uv')

    $sourceName = Get-UvSourceName -CommandName $CommandName
    if ($script:UvCompletionCache.ProbedPaths.ContainsKey($sourceName)) {
        return $script:UvCompletionCache.ExecutablePaths[$sourceName]
    }

    $script:UvCompletionCache.ProbedPaths[$sourceName] = $true

    $script:UvCompletionCache.ExecutablePaths[$sourceName] = $null
    $candidateNames = if ($sourceName -eq 'uvx') {
        @('uvx.exe', 'uvx')
    } else {
        @('uv.exe', 'uv')
    }

    foreach ($name in $candidateNames) {
        $command = Get-Command $name -ErrorAction Ignore
        if ($command) {
            $script:UvCompletionCache.ExecutablePaths[$sourceName] = $command.Source
            break
        }
    }

    $script:UvCompletionCache.ExecutablePaths[$sourceName]
}

function New-UvPathData {
    param(
        [string[]]$Commands = @(),
        [hashtable]$CommandDescriptions = @{},
        [string[]]$Options = @(),
        [System.Collections.IDictionary]$ValuesByOption = @{},
        [System.Collections.IDictionary]$MetavarByOption = @{},
        [string[]]$PositionalValues = @()
    )

    @{
        Commands            = @(@($Commands) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
        CommandDescriptions = $CommandDescriptions
        Options             = @(@($Options) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
        ValuesByOption      = $ValuesByOption
        MetavarByOption     = $MetavarByOption
        PositionalValues    = @(@($PositionalValues) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    }
}

function Get-UvTokenText {
    param([System.Management.Automation.Language.Ast]$Element)

    if ($Element -is [System.Management.Automation.Language.StringConstantExpressionAst]) {
        return $Element.Value
    }

    if ($Element -is [System.Management.Automation.Language.CommandParameterAst]) {
        return $Element.Extent.Text
    }

    $Element.Extent.Text
}

function Get-UvUniqueStrings {
    param(
        [string[]]$Items,
        [switch]$CaseSensitive
    )

    # Option tokens stay case-distinct (uv has -V/-v, -C/-c and -P/-p); commands and values fold.
    $comparer = if ($CaseSensitive) { [System.StringComparer]::Ordinal } else { [System.StringComparer]::OrdinalIgnoreCase }
    $seen = [System.Collections.Generic.HashSet[string]]::new($comparer)
    $result = New-Object System.Collections.Generic.List[string]

    foreach ($item in @($Items)) {
        if ([string]::IsNullOrWhiteSpace($item)) {
            continue
        }

        if (-not $seen.Add($item)) {
            continue
        }

        [void]$result.Add($item)
    }

    @($result.ToArray())
}

function Get-UvOptionTokensFromLine {
    param([string]$Line)

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

    $tokens = foreach ($match in [regex]::Matches($Line, '(?<!\S)(--?[A-Za-z0-9][A-Za-z0-9\-]*)(?=[\s,<\[]|\.\.\.|$|,)')) {
        $match.Groups[1].Value
    }

    Get-UvUniqueStrings -Items $tokens -CaseSensitive
}

function Get-UvCanonicalOption {
    param([string[]]$Tokens)

    foreach ($token in @($Tokens)) {
        if ($token.StartsWith('--')) {
            return $token
        }
    }

    if ($Tokens.Count -gt 0) {
        return $Tokens[0]
    }

    $null
}

function Add-UvPossibleValues {
    param(
        [System.Collections.IDictionary]$ValueMap,
        [string]$OptionKey,
        [string]$RawValueText
    )

    if ([string]::IsNullOrWhiteSpace($OptionKey) -or [string]::IsNullOrWhiteSpace($RawValueText)) {
        return
    }

    $valueText = $RawValueText
    if ($valueText -match '^(.*?)]') {
        $valueText = $matches[1]
    }
    $valueText = $valueText -replace '^(?:possible\s+values:|values:)\s*', ''

    $values = $valueText -split ',' |
        ForEach-Object { $_.Trim() } |
        Where-Object { -not [string]::IsNullOrWhiteSpace($_) }

    if (-not $ValueMap.ContainsKey($OptionKey)) {
        $ValueMap[$OptionKey] = @()
    }

    $ValueMap[$OptionKey] = Get-UvUniqueStrings -Items ($ValueMap[$OptionKey] + $values)
}

function Get-UvParsedHelpData {
    param([string[]]$HelpLines)

    $commands = New-Object System.Collections.Generic.List[string]
    $commandDescriptions = @{}
    $options = New-Object System.Collections.Generic.List[string]
    # Ordinal tables: a hashtable would fold -c onto -C and -p onto -P.
    $valuesByOption = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal)
    $metavarByOption = [System.Collections.Generic.Dictionary[string, string]]::new([System.StringComparer]::Ordinal)
    $positionalValues = New-Object System.Collections.Generic.List[string]
    $positionalKey = $null
    $section = ''
    $currentOption = $null
    $collectingValuesFor = $null
    $collectingValueListFor = $null
    $valueBuffer = ''

    foreach ($line in @($HelpLines)) {
        if ($collectingValuesFor) {
            $trimmed = $line.Trim()
            if ($trimmed) {
                if ($valueBuffer) {
                    $valueBuffer += ' '
                }
                $valueBuffer += $trimmed
            }

            if ($trimmed -match '\]') {
                Add-UvPossibleValues -ValueMap $valuesByOption -OptionKey $collectingValuesFor -RawValueText $valueBuffer
                $collectingValuesFor = $null
                $valueBuffer = ''
            }

            continue
        }

        if ($collectingValueListFor) {
            if ($line -match '^\s*$') {
                $collectingValueListFor = $null
            } elseif ($line -match '^\s*-\s+(.+?)(?:\s*:|\s*$)') {
                Add-UvPossibleValues -ValueMap $valuesByOption -OptionKey $collectingValueListFor -RawValueText $matches[1].Trim()
                continue
            } elseif ((@(Get-UvOptionTokensFromLine -Line $line)).Count -gt 0 -or
                $line -match '^\s*(Commands|Arguments|Examples?|Options|[A-Za-z][A-Za-z ]+ options):\s*$') {
                $collectingValueListFor = $null
            } elseif (-not ($line -match '^\s+')) {
                $collectingValueListFor = $null
            } else {
                continue
            }
        }

        if ($line -match '^\s*$') {
            $currentOption = $null
            continue
        }

        if ($line -match '^\s*(Commands|Arguments|Examples?|Options|[A-Za-z][A-Za-z ]+ options):\s*$') {
            $section = $matches[1].ToLowerInvariant()
            $currentOption = $null
            continue
        }

        if ($section -eq 'commands' -and $line -match '^\s{2,}([a-z][a-z0-9\-]*)\s{2,}(.+)$') {
            $command = $matches[1]
            [void]$commands.Add($command)
            $commandDescriptions[$command.ToLowerInvariant()] = $matches[2].Trim()
            continue
        }

        if ($section -eq 'arguments') {
            # Only the first positional's closed value set is modelled; the
            # existing wrapped '[possible values: ...]' collector handles the
            # continuation lines for it.
            if ($line -match '^\s{2,}(?:<([A-Z_]+)>|\[([A-Z_]+)\])') {
                $currentOption = $null
                $positionalKey = if ($positionalValues.Count -eq 0 -and $null -eq $positionalKey) { '<positional>' } else { $null }
            }

            if ($positionalKey -and $line -match '\[possible values:\s*(.+)$') {
                $valueBuffer = $matches[1].Trim()
                if ($valueBuffer -match '\]') {
                    Add-UvPossibleValues -ValueMap $valuesByOption -OptionKey $positionalKey -RawValueText $valueBuffer
                    $valueBuffer = ''
                } else {
                    $collectingValuesFor = $positionalKey
                }
            }

            continue
        }

        if ($section -like '*options') {
            $tokens = @(Get-UvOptionTokensFromLine -Line $line)
            if ($tokens.Count -gt 0) {
                foreach ($token in $tokens) {
                    [void]$options.Add($token)
                }

                $currentOption = Get-UvCanonicalOption -Tokens $tokens

                # A metavariable after the option names marks a value-bearing
                # option; its absence marks a switch.
                $metavar = if ($line -match '^\s*-[^\s]+(?:,\s*-[^\s]+)*(?:\.\.\.)?\s+(?:\[=)?<([A-Z][A-Z0-9_]*)>') { $matches[1] } else { '' }
                foreach ($token in $tokens) {
                    $metavarByOption[$token] = $metavar
                }
            }

            if ($currentOption -and $line -match '\[possible values:\s*(.+)$') {
                $valueBuffer = $matches[1].Trim()
                if ($valueBuffer -match '\]') {
                    Add-UvPossibleValues -ValueMap $valuesByOption -OptionKey $currentOption -RawValueText $valueBuffer
                    $valueBuffer = ''
                } else {
                    $collectingValuesFor = $currentOption
                }
            } elseif ($currentOption -and $line -match '\[possible\s*$') {
                $valueBuffer = ''
                $collectingValuesFor = $currentOption
            } elseif ($currentOption -and $line -match '^\s*Possible values:\s*$') {
                $collectingValueListFor = $currentOption
            }
        }
    }

    if ($collectingValuesFor -and $valueBuffer) {
        Add-UvPossibleValues -ValueMap $valuesByOption -OptionKey $collectingValuesFor -RawValueText $valueBuffer
    }

    if ($valuesByOption.ContainsKey('<positional>')) {
        foreach ($value in @($valuesByOption['<positional>'])) {
            [void]$positionalValues.Add($value)
        }
        $valuesByOption.Remove('<positional>')
    }

    @{
        Commands            = Get-UvUniqueStrings -Items $commands.ToArray()
        CommandDescriptions = $commandDescriptions
        Options             = Get-UvUniqueStrings -Items $options.ToArray() -CaseSensitive
        ValuesByOption      = $valuesByOption
        MetavarByOption     = $metavarByOption
        PositionalValues    = Get-UvUniqueStrings -Items $positionalValues.ToArray()
    }
}

function Invoke-UvHelp {
    param(
        [string]$CommandName = 'uv',
        [string[]]$Path
    )

    $uvPath = Get-UvExecutablePath -CommandName $CommandName
    if (-not $uvPath) {
        return @()
    }

    $pathItems = @($Path)
    if ($null -eq $Path) {
        $pathItems = @()
    }
    $pathItems = @($pathItems | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })

    $arguments = @()
    if (@($pathItems).Count -gt 0) {
        $arguments += $pathItems
    }
    $arguments += '--help'

    try {
        $startInfo = [System.Diagnostics.ProcessStartInfo]::new()
        $startInfo.FileName = $uvPath
        foreach ($argument in $arguments) {
            [void]$startInfo.ArgumentList.Add($argument)
        }
        $startInfo.UseShellExecute = $false
        $startInfo.CreateNoWindow = $true
        $startInfo.RedirectStandardInput = $true
        $startInfo.RedirectStandardOutput = $true
        $startInfo.RedirectStandardError = $true

        $process = [System.Diagnostics.Process]::Start($startInfo)
        try {
            $process.StandardInput.Close()
            $outputTask = $process.StandardOutput.ReadToEndAsync()
            [void]$process.StandardError.ReadToEndAsync()
            if (-not $process.WaitForExit(5000)) {
                try { $process.Kill($true) } catch { Write-Debug -Message $_.Exception.Message }
                return @()
            }

            $text = ($outputTask.Result -replace '\e\[[0-9;?]*[ -/]*[@-~]', '')
            if ([string]::IsNullOrEmpty($text)) {
                return @()
            }

            @($text -split '\r?\n')
        } finally {
            $process.Dispose()
        }
    } catch {
        @()
    }
}

function Get-UvStaticSubcommands {
    param([string[]]$Path)

    $key = Get-UvCacheKey -Path $Path
    if ($script:UvCompletionCache.StaticTree.ContainsKey($key)) {
        return $script:UvCompletionCache.StaticTree[$key]
    }

    @()
}

function Get-UvPathData {
    param(
        [string]$SourceName = 'uv',
        [string[]]$Path
    )

    $path = @($Path)
    if ($null -eq $Path) {
        $path = @()
    }
    $path = @($path | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })

    $key = Get-UvPathDataCacheKey -SourceName $SourceName -Path $path
    if ($script:UvCompletionCache.PathData.ContainsKey($key)) {
        return $script:UvCompletionCache.PathData[$key]
    }

    $helpLines = @()
    $pathKey = Get-UvCacheKey -Path $path
    $uvxRootKey = Get-UvCacheKey -Path @('tool', 'run')
    if ($SourceName -eq 'uvx' -and $pathKey -eq $uvxRootKey) {
        $helpLines += @(Invoke-UvHelp -CommandName 'uvx')
        $helpLines += ''
        $helpLines += @(Invoke-UvHelp -CommandName 'uv' -Path $path)
    } else {
        $helpLines += @(Invoke-UvHelp -CommandName $SourceName -Path $path)
    }

    $parsed = if ($helpLines.Count -gt 0) {
        Get-UvParsedHelpData -HelpLines $helpLines
    } else {
        New-UvPathData
    }

    $staticCommands = Get-UvStaticSubcommands -Path $path
    $data = New-UvPathData `
        -Commands (Get-UvUniqueStrings -Items ($staticCommands + $parsed.Commands)) `
        -CommandDescriptions $parsed.CommandDescriptions `
        -Options $parsed.Options `
        -ValuesByOption $parsed.ValuesByOption `
        -MetavarByOption $parsed.MetavarByOption `
        -PositionalValues $parsed.PositionalValues

    $script:UvCompletionCache.PathData[$key] = $data
    $data
}

function Find-UvSubcommand {
    param(
        [string]$SourceName = 'uv',
        [string[]]$Path,
        [string]$Token
    )

    foreach ($command in (Get-UvPathData -SourceName $SourceName -Path $Path).Commands) {
        if ($command.Equals($Token, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $command
        }
    }

    $null
}

function Get-UvCommandContext {
    param(
        [string]$WordToComplete,
        [System.Management.Automation.Language.CommandAst]$CommandAst,
        [int]$CursorPosition,
        [string]$SourceName = 'uv',
        [string[]]$RootPath = @(),
        [bool]$SupportsHelpCommand = $true
    )

    $tokens = New-Object System.Collections.Generic.List[string]
    $lastElementBeforeCursor = $null

    for ($index = 1; $index -lt $CommandAst.CommandElements.Count; $index++) {
        $element = $CommandAst.CommandElements[$index]
        if ($element.Extent.StartOffset -ge $CursorPosition) {
            continue
        }

        $lastElementBeforeCursor = $element
        $token = Get-UvTokenText -Element $element
        if ([string]::IsNullOrWhiteSpace($token)) {
            continue
        }

        [void]$tokens.Add($token)
    }

    # Trailing whitespace is judged against the last element BEFORE the
    # cursor, so text after the cursor does not shift the command path.
    $hasTrailingSpace = $false
    if ([string]::IsNullOrEmpty($WordToComplete)) {
        $hasTrailingSpace = if ($null -ne $lastElementBeforeCursor) {
            $CursorPosition -gt $lastElementBeforeCursor.Extent.EndOffset
        } else {
            $CursorPosition -gt $CommandAst.CommandElements[0].Extent.EndOffset
        }
    }

    $previousToken = $null
    if ($tokens.Count -gt 0) {
        if ($hasTrailingSpace) {
            $previousToken = $tokens[$tokens.Count - 1]
        } elseif ($tokens.Count -gt 1) {
            $previousToken = $tokens[$tokens.Count - 2]
        }
    }

    $pathTokens = @()
    if ($hasTrailingSpace) {
        $pathTokens = $tokens.ToArray()
    } elseif ($tokens.Count -gt 1) {
        $pathTokens = $tokens.GetRange(0, $tokens.Count - 1).ToArray()
    }

    # An empty synthetic root arrives as $null once unrolled, so filter
    # blank segments before they can become a bogus path element.
    $commandPath = New-Object System.Collections.Generic.List[string]
    foreach ($token in @($RootPath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })) {
        [void]$commandPath.Add($token)
    }
    $rootPathCount = $commandPath.Count

    $helpMode = $false
    $operandSeen = $false
    $pendingValueOption = $null

    foreach ($token in @($pathTokens)) {
        if ($null -ne $pendingValueOption) {
            $pendingValueOption = $null
            continue
        }

        if ($token.StartsWith('-')) {
            if (-not $token.Contains('=') -and (Get-UvOptionValueKind -SourceName $SourceName -Path $commandPath.ToArray() -Option $token) -ne 'switch') {
                $pendingValueOption = $token
            }

            continue
        }

        if ($operandSeen) {
            continue
        }

        if ($SupportsHelpCommand -and
            $commandPath.Count -eq $rootPathCount -and
            -not $helpMode -and
            $token.Equals('help', [System.StringComparison]::OrdinalIgnoreCase)) {
            $helpMode = $true
            continue
        }

        $nextCommand = Find-UvSubcommand -SourceName $SourceName -Path $commandPath.ToArray() -Token $token
        if (-not $nextCommand -and
            $SourceName -eq 'uv' -and
            $commandPath.Count -eq 0 -and
            -not $helpMode -and
            $token -match '^[a-z][a-z0-9-]*$') {
            # Hidden root commands (generate-shell-completion) are absent from
            # `uv --help`; `uv <token> --help` is safe at the root because uv
            # rejects unknown subcommands instead of running anything.
            $probe = Get-UvPathData -SourceName $SourceName -Path @($token)
            if (@($probe.Commands).Count -gt 0 -or @($probe.Options).Count -gt 0) {
                $nextCommand = $token
            }
        }

        if ($nextCommand) {
            [void]$commandPath.Add($nextCommand)
        } else {
            $operandSeen = $true
        }
    }

    @{
        Path            = if ($commandPath.Count -gt 0) { @($commandPath.ToArray()) } else { @() }
        PreviousToken   = $previousToken
        HelpMode        = $helpMode
        HasTrailingSpace = $hasTrailingSpace
        OperandSeen     = $operandSeen
    }
}

function Get-UvOptionValues {
    param(
        [string]$SourceName = 'uv',
        [string[]]$Path,
        [string]$Option
    )

    if ([string]::IsNullOrWhiteSpace($Option) -or -not $Option.StartsWith('-')) {
        return @()
    }

    $optionKey = $Option
    $values = @()

    $pathData = Get-UvPathData -SourceName $SourceName -Path $Path
    if ($pathData.ValuesByOption.ContainsKey($optionKey)) {
        $values += $pathData.ValuesByOption[$optionKey]
    }

    if (@($values).Count -eq 0) {
        $rootData = Get-UvPathData -SourceName $SourceName -Path @()
        if ($rootData.ValuesByOption.ContainsKey($optionKey)) {
            $values += $rootData.ValuesByOption[$optionKey]
        }
    }

    Get-UvUniqueStrings -Items $values
}

function Get-UvOptionMetavar {
    param(
        [string]$SourceName = 'uv',
        [string[]]$Path,
        [string]$Option
    )

    $optionKey = $Option
    foreach ($data in @((Get-UvPathData -SourceName $SourceName -Path $Path), (Get-UvPathData -SourceName $SourceName -Path @()))) {
        if ($data.MetavarByOption.ContainsKey($optionKey)) {
            return [string]$data.MetavarByOption[$optionKey]
        }
    }

    $null
}

function Get-UvOptionValueKind {
    param(
        [string]$SourceName = 'uv',
        [string[]]$Path,
        [string]$Option
    )

    # 'switch' for options without a metavariable (or unknown options),
    # 'path' for file/directory metavariables, otherwise 'value'.
    if ([string]::IsNullOrWhiteSpace($Option) -or -not $Option.StartsWith('-')) {
        return 'switch'
    }

    $metavar = Get-UvOptionMetavar -SourceName $SourceName -Path $Path -Option $Option
    if ([string]::IsNullOrEmpty($metavar)) {
        return 'switch'
    }

    if ($metavar -match '(^|_)(DIR|DIRECTORY|FILE|PATH|REQUIREMENTS|CONSTRAINTS|OVERRIDES|SCRIPT|PROJECT|CACHE_DIR|OUTPUT_FILE)$') {
        return 'path'
    }

    'value'
}

function Get-UvOptionAssignmentContext {
    param([string]$WordToComplete)

    if ($WordToComplete -match '^(--?[A-Za-z0-9][A-Za-z0-9\-]*)=(.*)$') {
        return @{
            Option      = $matches[1]
            ValuePrefix = $matches[2]
        }
    }

    $null
}

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

    if (-not [string]::IsNullOrEmpty($WordToComplete)) {
        return $WordToComplete
    }

    if ($CommandAst.CommandElements.Count -eq 0) {
        return $WordToComplete
    }

    foreach ($element in @($CommandAst.CommandElements | Select-Object -Skip 1)) {
        if ($element.Extent.EndOffset -eq $CursorPosition) {
            return Get-UvTokenText -Element $element
        }
    }

    $WordToComplete
}

function New-UvCompletionResults {
    param(
        [string[]]$Items,
        [System.Management.Automation.CompletionResultType]$ResultType,
        [string]$WordToComplete,
        [hashtable]$Tooltips
    )

    foreach ($item in @($Items | Where-Object { -not [string]::IsNullOrEmpty($_) })) {
        # A typed short flag must keep its case so -C never completes to -c; long options and
        # values keep the usual case-insensitive prefix match.
        $pattern = [System.Management.Automation.WildcardPattern]::Escape($WordToComplete) + '*'
        $matched = if ($WordToComplete -cmatch '^-[^-]') { $item -clike $pattern } else { $item -like $pattern }
        if (-not $matched) {
            continue
        }

        $tooltip = if ($Tooltips -and $Tooltips.ContainsKey($item.ToLowerInvariant())) {
            $Tooltips[$item.ToLowerInvariant()]
        } else {
            $item
        }

        [System.Management.Automation.CompletionResult]::new($item, $item, $ResultType, $tooltip)
    }
}

function New-UvAssignedValueCompletionResults {
    param(
        [string]$Option,
        [string[]]$Items,
        [string]$ValuePrefix
    )

    foreach ($item in @($Items | Where-Object { -not [string]::IsNullOrEmpty($_) })) {
        if ($item -notlike ([System.Management.Automation.WildcardPattern]::Escape($ValuePrefix) + '*')) {
            continue
        }

        $completionText = '{0}={1}' -f $Option, $item
        [System.Management.Automation.CompletionResult]::new(
            $completionText,
            $completionText,
            [System.Management.Automation.CompletionResultType]::ParameterValue,
            $completionText
        )
    }
}

function Complete-Uv {
    param($wordToComplete, $commandAst, $cursorPosition)

    $sourceName = 'uv'
    if ($commandAst.CommandElements.Count -gt 0) {
        $sourceName = Get-UvSourceName -CommandName (Get-UvTokenText -Element $commandAst.CommandElements[0])
    }

    $effectiveWordToComplete = Get-UvEffectiveWordToComplete `
        -WordToComplete $wordToComplete `
        -CommandAst $commandAst `
        -CursorPosition $cursorPosition

    $context = Get-UvCommandContext `
        -WordToComplete $effectiveWordToComplete `
        -CommandAst $commandAst `
        -CursorPosition $cursorPosition `
        -SourceName $sourceName `
        -RootPath (Get-UvSyntheticRootPath -SourceName $sourceName) `
        -SupportsHelpCommand ($sourceName -ne 'uvx')
    $path = $context.Path
    $previousToken = $context.PreviousToken
    $pathData = Get-UvPathData -SourceName $sourceName -Path $path
    $assignmentContext = Get-UvOptionAssignmentContext -WordToComplete $effectiveWordToComplete

    if ($assignmentContext) {
        New-UvAssignedValueCompletionResults `
            -Option $assignmentContext.Option `
            -Items (Get-UvOptionValues -SourceName $sourceName -Path $path -Option $assignmentContext.Option) `
            -ValuePrefix $assignmentContext.ValuePrefix
        return
    }

    if ($previousToken -and $previousToken.StartsWith('-') -and -not $previousToken.Contains('=')) {
        $valueKind = Get-UvOptionValueKind -SourceName $sourceName -Path $path -Option $previousToken
        if ($valueKind -eq 'path') {
            # A path-typed option: the engine's own filesystem completion is
            # the intended answer, so return nothing on purpose.
            return @()
        }

        if ($valueKind -eq 'value') {
            $values = @(Get-UvOptionValues -SourceName $sourceName -Path $path -Option $previousToken)
            if ($values.Count -eq 0) {
                $metavar = Get-UvOptionMetavar -SourceName $sourceName -Path $path -Option $previousToken
                $values = @('<' + $metavar.ToLowerInvariant() + '>')
            }

            New-UvCompletionResults -Items $values -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -WordToComplete $wordToComplete -Tooltips @{}
            return
        }
    }

    if ($context.HelpMode) {
        New-UvCompletionResults -Items $pathData.Commands -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -WordToComplete $wordToComplete -Tooltips $pathData.CommandDescriptions
        return
    }

    if ($effectiveWordToComplete.StartsWith('-')) {
        New-UvCompletionResults -Items $pathData.Options -ResultType ([System.Management.Automation.CompletionResultType]::ParameterName) -WordToComplete $effectiveWordToComplete -Tooltips @{}
        return
    }

    if (-not $context.OperandSeen -and @($pathData.PositionalValues).Count -gt 0) {
        New-UvCompletionResults -Items $pathData.PositionalValues -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -WordToComplete $effectiveWordToComplete -Tooltips @{}
        return
    }

    if (-not $context.OperandSeen) {
        New-UvCompletionResults -Items $pathData.Commands -ResultType ([System.Management.Automation.CompletionResultType]::ParameterValue) -WordToComplete $effectiveWordToComplete -Tooltips $pathData.CommandDescriptions
    }

    New-UvCompletionResults -Items $pathData.Options -ResultType ([System.Management.Automation.CompletionResultType]::ParameterName) -WordToComplete $effectiveWordToComplete -Tooltips @{}
}

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

    Complete-Uv -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition
}