completers/cargo_binstall_completer/cargo_binstall_completer.ps1
|
Set-StrictMode -Version 2.0 if ($true) { function Get-CargoBinstallCompleterCache { if (Test-Path -LiteralPath variable:script:CargoBinstallCompleterCache) { return $script:CargoBinstallCompleterCache } $cache = @{ CommandPath = $null HelpDefinition = $null TargetTriples = $null } Set-Variable -Name CargoBinstallCompleterCache -Scope Script -Value $cache $cache } function Resolve-CargoBinstallCommandPath { $cache = Get-CargoBinstallCompleterCache if ($cache.CommandPath) { return $cache.CommandPath } $command = Get-Command -Name 'cargo-binstall.exe', 'cargo-binstall' -ErrorAction SilentlyContinue | Select-Object -First 1 if ($command) { $cache.CommandPath = if ($command.Source) { $command.Source } else { $command.Name } } $cache.CommandPath } function Invoke-CargoBinstallText { param([string[]]$Arguments) $commandPath = Resolve-CargoBinstallCommandPath if (-not $commandPath) { return @() } try { @(& $commandPath @Arguments 2>&1 | ForEach-Object { $_.ToString() -replace '\e\[[0-9;?]*[ -/]*[@-~]', '' }) } catch { @() } } function Get-CargoBinstallHelpDefinition { $cache = Get-CargoBinstallCompleterCache if ($cache.HelpDefinition) { return $cache.HelpDefinition } $definition = @{ Options = @() OptionMap = @{} PositionalPlaceholder = '<crate[@version]>' } $lines = @(Invoke-CargoBinstallText -Arguments @('--help')) $currentEntry = $null foreach ($line in $lines) { if ($line -match '^\s{2,}(?<spec>(?:(?:-[A-Za-z0-9?],\s*)?--[A-Za-z0-9][A-Za-z0-9\-]*|-[A-Za-z0-9?])(?:\s+<[^>]+>)?(?:\.\.\.)?)\s*$') { if ($currentEntry) { $definition.Options += $currentEntry foreach ($name in $currentEntry.Names) { $definition.OptionMap[$name] = $currentEntry } } $spec = $matches['spec'].Trim() $names = @([regex]::Matches($spec, '(?:^|,\s*)(--[A-Za-z0-9][A-Za-z0-9\-]*|-[A-Za-z0-9?])') | ForEach-Object { $_.Groups[1].Value }) $takesValue = $spec -match '<(?<placeholder>[^>]+)>' $placeholder = if ($takesValue) { '<' + $matches['placeholder'] + '>' } else { $null } $currentEntry = [pscustomobject]@{ Names = $names PrimaryName = $names[-1] Description = '' TakesValue = $takesValue ValuePlaceholder = $placeholder } continue } if ($line -match '^\s{2,}\[crate\[@version\]\]\.\.\.$') { $definition.PositionalPlaceholder = '<crate[@version]>' continue } if ($currentEntry) { if ([string]::IsNullOrWhiteSpace($line)) { $definition.Options += $currentEntry foreach ($name in $currentEntry.Names) { $definition.OptionMap[$name] = $currentEntry } $currentEntry = $null continue } if ($line -match '^\S') { $definition.Options += $currentEntry foreach ($name in $currentEntry.Names) { $definition.OptionMap[$name] = $currentEntry } $currentEntry = $null } elseif ((-not $currentEntry.Description) -and $line -match '^\s{2,}(?<description>\S.*)$') { $currentEntry = [pscustomobject]@{ Names = $currentEntry.Names PrimaryName = $currentEntry.PrimaryName Description = $matches['description'].Trim() TakesValue = $currentEntry.TakesValue ValuePlaceholder = $currentEntry.ValuePlaceholder } } } } if ($currentEntry) { $definition.Options += $currentEntry foreach ($name in $currentEntry.Names) { $definition.OptionMap[$name] = $currentEntry } } $cache.HelpDefinition = $definition $definition } function Get-CargoBinstallTargetTriples { $cache = Get-CargoBinstallCompleterCache if ($cache.TargetTriples) { return $cache.TargetTriples } $rustc = Get-Command -Name 'rustc.exe', 'rustc' -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $rustc) { $cache.TargetTriples = @() return $cache.TargetTriples } try { $cache.TargetTriples = @(& $rustc.Source '--print' 'target-list' 2>$null | Where-Object { $_ } | Sort-Object -Unique) } catch { $cache.TargetTriples = @() } $cache.TargetTriples } function New-CargoBinstallCompletionResult { param( [string]$CompletionText, [string]$ListItemText, [string]$ResultType, [string]$ToolTip ) if ([string]::IsNullOrWhiteSpace($ListItemText)) { $ListItemText = $CompletionText } if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $CompletionText } [System.Management.Automation.CompletionResult]::new( $CompletionText, $ListItemText, $ResultType, $ToolTip ) } function Remove-CargoBinstallOuterQuotes { param([string]$Value) if ($null -eq $Value) { return '' } $Value.Trim([char[]]@([char]34, [char]39)) } function ConvertFrom-CargoBinstallTypedWord { # 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 ConvertTo-CargoBinstallQuotedValue { # 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 starting with a dash would be parsed 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-CargoBinstallCurrentToken { param( [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) # The parser keeps an unterminated quoted word as one element running to the cursor. foreach ($element in ($CommandAst.CommandElements | Select-Object -Skip 1)) { $extent = $element.Extent if ($extent.StartOffset -lt $CursorPosition -and $CursorPosition -le $extent.EndOffset) { return $extent.Text.Substring(0, $CursorPosition - $extent.StartOffset) } } '' } function Get-CargoBinstallTokensBeforeCursor { param( [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) $tokens = New-Object System.Collections.Generic.List[string] foreach ($element in $CommandAst.CommandElements | Select-Object -Skip 1) { if ($element.Extent.EndOffset -lt $CursorPosition) { $tokens.Add($element.Extent.Text) } } @($tokens.ToArray()) } function Get-CargoBinstallPathCompletions { param([string]$InputPath) $cleanInput = ConvertFrom-CargoBinstallTypedWord -Value $InputPath $quoteChar = if ($InputPath -match '^[''"\u2018-\u201E]') { $InputPath.Substring(0, 1) } else { '' } # CompleteFilename rewrites a typed ./ or ../ as .\ or ..\; the typed prefix is kept. $typedPrefix = if ($cleanInput -match '^\.{1,2}[\\/]') { $Matches[0] } else { '' } [System.Management.Automation.CompletionCompleters]::CompleteFilename($cleanInput) | ForEach-Object { # CompleteFilename quotes for PowerShell and wildcard-escapes for -Path # parameters (tick``x.txt); cargo-binstall takes literal paths, so the # result is unwrapped by the parser, unescaped, and quoted once here. $path = $_.CompletionText if ($path -match '^[''"\u2018-\u201E]') { $ast = [System.Management.Automation.Language.Parser]::ParseInput($path, [ref]$null, [ref]$null) $constant = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true) if ($constant) { $path = $constant.Value } } $path = [System.Management.Automation.WildcardPattern]::Unescape($path) if ($typedPrefix -and -not $path.StartsWith($typedPrefix)) { $path = $typedPrefix + ($path -replace '^\.{1,2}[\\/]', '') } $completionText = ConvertTo-CargoBinstallQuotedValue -Value $path -QuoteChar $quoteChar New-CargoBinstallCompletionResult -CompletionText $completionText -ListItemText $_.ListItemText -ResultType $_.ResultType -ToolTip $_.ToolTip } } function Get-CargoBinstallPlaceholderCompletion { param( [string]$Placeholder, [string]$CurrentWord, [string]$ToolTip ) # The placeholder names an empty slot; once text is typed there is nothing # to add, and echoing it back would only hide the engine's own fallback. if (-not [string]::IsNullOrWhiteSpace($CurrentWord) -and -not $Placeholder.StartsWith($CurrentWord, [System.StringComparison]::OrdinalIgnoreCase)) { return } New-CargoBinstallCompletionResult -CompletionText $Placeholder -ListItemText $Placeholder -ResultType 'ParameterValue' -ToolTip $ToolTip } function Get-CargoBinstallNamedValueCompletions { param( [string[]]$Values, [string]$CurrentWord, [string]$ToolTip ) $cleanCurrent = Remove-CargoBinstallOuterQuotes -Value $CurrentWord foreach ($value in $Values) { if ($value.StartsWith($cleanCurrent, [System.StringComparison]::OrdinalIgnoreCase)) { New-CargoBinstallCompletionResult -CompletionText $value -ListItemText $value -ResultType 'ParameterValue' -ToolTip $ToolTip } } } function Get-CargoBinstallDelimitedValueCompletions { param( [string[]]$Values, [string]$CurrentWord, [string]$ToolTip ) $cleanCurrent = Remove-CargoBinstallOuterQuotes -Value $CurrentWord $segments = @(if ([string]::IsNullOrWhiteSpace($cleanCurrent)) { '' } else { $cleanCurrent.Split(',') }) $selected = if ($segments.Count -gt 1) { @($segments[0..($segments.Count - 2)] | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } else { @() } $fragment = $segments[-1].Trim() $prefix = if ($segments.Count -gt 1) { (($segments[0..($segments.Count - 2)] | ForEach-Object { $_.Trim() }) -join ',') + ',' } else { '' } foreach ($value in $Values) { if (($selected -contains $value) -or (-not $value.StartsWith($fragment, [System.StringComparison]::OrdinalIgnoreCase))) { continue } $completionText = $prefix + $value New-CargoBinstallCompletionResult -CompletionText $completionText -ListItemText $completionText -ResultType 'ParameterValue' -ToolTip $ToolTip } } function Get-CargoBinstallOptionValueCompletions { param( [string]$OptionName, [string]$CurrentWord ) switch ($OptionName) { '--manifest-path' { return @(Get-CargoBinstallPathCompletions -InputPath $CurrentWord) } '--bin-dir' { return @(Get-CargoBinstallPathCompletions -InputPath $CurrentWord) } '--install-path' { return @(Get-CargoBinstallPathCompletions -InputPath $CurrentWord) } '--root' { return @(Get-CargoBinstallPathCompletions -InputPath $CurrentWord) } '--root-certificates' { return @(Get-CargoBinstallPathCompletions -InputPath $CurrentWord) } '--settings' { return @(Get-CargoBinstallPathCompletions -InputPath $CurrentWord) } '--pkg-fmt' { return @(Get-CargoBinstallNamedValueCompletions -Values @('tar', 'tbz2', 'tgz', 'txz', 'tzstd', 'zip', 'bin') -CurrentWord $CurrentWord -ToolTip 'cargo-binstall package format') } '--strategies' { return @(Get-CargoBinstallDelimitedValueCompletions -Values @('crate-meta-data', 'quick-install', 'compile') -CurrentWord $CurrentWord -ToolTip 'cargo-binstall strategy list') } '--disable-strategies' { return @(Get-CargoBinstallDelimitedValueCompletions -Values @('crate-meta-data', 'quick-install', 'compile') -CurrentWord $CurrentWord -ToolTip 'cargo-binstall strategy list') } '--min-tls-version' { return @(Get-CargoBinstallNamedValueCompletions -Values @('1.2', '1.3') -CurrentWord $CurrentWord -ToolTip 'Minimum TLS version') } '--log-level' { return @(Get-CargoBinstallNamedValueCompletions -Values @('trace', 'debug', 'info', 'warn', 'error', 'off') -CurrentWord $CurrentWord -ToolTip 'Utility log level') } '--targets' { $targetTriples = @(Get-CargoBinstallTargetTriples) if ($targetTriples.Count -gt 0) { return @(Get-CargoBinstallDelimitedValueCompletions -Values $targetTriples -CurrentWord $CurrentWord -ToolTip 'Rust target triples') } } } $definition = Get-CargoBinstallHelpDefinition if ($definition.OptionMap.ContainsKey($OptionName)) { $entry = $definition.OptionMap[$OptionName] if ($entry.ValuePlaceholder) { return @( Get-CargoBinstallPlaceholderCompletion -Placeholder $entry.ValuePlaceholder -CurrentWord $CurrentWord -ToolTip ("cargo-binstall value for $OptionName") ) } } @() } function Get-CargoBinstallOptionCompletions { param([string]$CurrentWord) $definition = Get-CargoBinstallHelpDefinition $cleanCurrent = Remove-CargoBinstallOuterQuotes -Value $CurrentWord foreach ($entry in $definition.Options) { foreach ($name in $entry.Names) { if ($name.StartsWith($cleanCurrent, [System.StringComparison]::OrdinalIgnoreCase)) { New-CargoBinstallCompletionResult -CompletionText $name -ListItemText $name -ResultType 'ParameterName' -ToolTip $entry.Description } } } } function Get-CargoBinstallPositionalCompletions { param([string]$CurrentWord) $definition = Get-CargoBinstallHelpDefinition @( Get-CargoBinstallPlaceholderCompletion -Placeholder $definition.PositionalPlaceholder -CurrentWord $CurrentWord -ToolTip 'crate name or crate@version' ) } function Get-CargoBinstallState { param([string[]]$TokensBeforeCurrent) $definition = Get-CargoBinstallHelpDefinition $pendingOption = $null $positionals = New-Object System.Collections.Generic.List[string] $afterDoubleDash = $false foreach ($rawToken in $TokensBeforeCurrent) { $token = Remove-CargoBinstallOuterQuotes -Value $rawToken if ([string]::IsNullOrWhiteSpace($token)) { continue } if ($pendingOption) { $pendingOption = $null continue } if ($afterDoubleDash) { $positionals.Add($token) continue } if ($token -eq '--') { $afterDoubleDash = $true continue } if ($definition.OptionMap.ContainsKey($token)) { $entry = $definition.OptionMap[$token] if ($entry.TakesValue) { $pendingOption = $entry.PrimaryName } continue } if ($token.StartsWith('-')) { continue } $positionals.Add($token) } [pscustomobject]@{ PendingOption = $pendingOption Positionals = @($positionals.ToArray()) AfterDoubleDash = $afterDoubleDash } } function Complete-CargoBinstall { param( [string]$wordToComplete, [System.Management.Automation.Language.CommandAst]$commandAst, [int]$cursorPosition ) $currentWord = if ($cursorPosition -gt $commandAst.Extent.EndOffset) { '' } else { Get-CargoBinstallCurrentToken -CommandAst $commandAst -CursorPosition $cursorPosition } $tokensBeforeCurrent = @(Get-CargoBinstallTokensBeforeCursor -CommandAst $commandAst -CursorPosition $cursorPosition) if (($tokensBeforeCurrent.Count -eq 0) -and ($currentWord -match '^(?i)cargo-binstall(?:\.exe)?$')) { $currentWord = '' } $state = Get-CargoBinstallState -TokensBeforeCurrent $tokensBeforeCurrent if ($state.PendingOption) { return @(Get-CargoBinstallOptionValueCompletions -OptionName $state.PendingOption -CurrentWord $currentWord) } if ($state.AfterDoubleDash) { # Everything after '--' is a crate operand. return @(Get-CargoBinstallPositionalCompletions -CurrentWord $currentWord) } if (-not [string]::IsNullOrEmpty($currentWord) -and $currentWord.StartsWith('-')) { # The value keeps any typed opening quote so the completion keeps it too. $attached = [regex]::Match($currentWord, '^(?<name>--[^=]+)=(?<value>.*)$') if ($attached.Success) { # Attached --opt=value form: complete the value, keeping the prefix. $definition = Get-CargoBinstallHelpDefinition $attachedName = $attached.Groups['name'].Value if (-not $definition.OptionMap.ContainsKey($attachedName) -or -not $definition.OptionMap[$attachedName].TakesValue) { return @() } $attachedPrefix = $attachedName + '=' return @(Get-CargoBinstallOptionValueCompletions -OptionName $definition.OptionMap[$attachedName].PrimaryName -CurrentWord $attached.Groups['value'].Value | ForEach-Object { New-CargoBinstallCompletionResult -CompletionText ($attachedPrefix + $_.CompletionText) -ListItemText $_.ListItemText -ResultType $_.ResultType -ToolTip $_.ToolTip }) } return @(Get-CargoBinstallOptionCompletions -CurrentWord $currentWord) } $results = New-Object System.Collections.Generic.List[object] if ([string]::IsNullOrEmpty($currentWord)) { foreach ($item in (Get-CargoBinstallOptionCompletions -CurrentWord '')) { $results.Add($item) } } foreach ($item in (Get-CargoBinstallPositionalCompletions -CurrentWord $currentWord)) { $results.Add($item) } @($results.ToArray()) } } Register-ArgumentCompleter -Native -CommandName @('cargo-binstall', 'cargo-binstall.exe') -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-CargoBinstall -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } |