completers/rg_completer/rg_completer.ps1
|
# rg tab completion for PowerShell # Builds a help-driven option catalog for rg.exe and adds value-aware completion. Set-StrictMode -Version 2.0 function New-RgCompletionResult { param( [string]$CompletionText, [string]$ResultType, [string]$ToolTip, [string]$ListItemText ) if ([string]::IsNullOrWhiteSpace($ListItemText)) { $ListItemText = $CompletionText } if ([string]::IsNullOrWhiteSpace($ToolTip)) { $ToolTip = $CompletionText } [System.Management.Automation.CompletionResult]::new( $CompletionText, $ListItemText, $ResultType, $ToolTip ) } function Get-RgUniqueStrings { param([string[]]$Items) $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $results = New-Object System.Collections.Generic.List[string] foreach ($item in @($Items)) { if ([string]::IsNullOrWhiteSpace($item)) { continue } if ($seen.Add($item)) { [void]$results.Add($item) } } @($results.ToArray()) } function Get-RgDefaultEncodingSuggestions { @( 'auto', 'none', 'utf-8', 'utf8', 'utf-16', 'utf-16le', 'utf-16be', 'utf-32', 'utf-32le', 'utf-32be', 'ascii', 'latin1', 'windows-1252', 'shift_jis', 'euc-jp', 'gbk', 'big5' ) } function Get-RgGenerateKinds { @( 'man', 'complete-bash', 'complete-zsh', 'complete-fish', 'complete-powershell' ) } function Get-RgHyperlinkFormats { @( 'default', 'none', 'cursor', 'file', 'grep+', 'kitty', 'macvim', 'textmate', 'vscode', 'vscode-insiders', 'vscodium' ) } function Get-RgColorWhenValues { @('never', 'auto', 'always', 'ansi') } function Get-RgSortValues { @('none', 'path', 'modified', 'accessed', 'created') } function Get-RgColorSpecSuggestions { @( 'path:fg:cyan', 'line:fg:green', 'column:fg:yellow', 'match:fg:magenta', 'highlight:bg:yellow', 'highlight:fg:black', 'path:none' ) } function Get-RgCompletionCatalog { if (Get-Variable -Name RgCompletionCatalog -Scope Script -ErrorAction Ignore) { return $script:RgCompletionCatalog } $script:RgCompletionCatalog = @{ Initialized = $false CommandName = $null Options = @() OptionByToken = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::Ordinal) TypeNames = @() EncodingSuggestions = Get-RgDefaultEncodingSuggestions GenerateKinds = Get-RgGenerateKinds HyperlinkFormats = Get-RgHyperlinkFormats ColorWhenValues = Get-RgColorWhenValues SortValues = Get-RgSortValues ColorSpecSuggestions = Get-RgColorSpecSuggestions } $script:RgCompletionCatalog } function Resolve-RgCommandName { $catalog = Get-RgCompletionCatalog if ($catalog.CommandName) { return $catalog.CommandName } $command = Get-Command -Name rg.exe, rg -ErrorAction Ignore | Select-Object -First 1 if ($command) { $catalog.CommandName = if ($command.Source) { $command.Source } else { $command.Name } } $catalog.CommandName } function Invoke-RgCapture { param([string[]]$Arguments) $commandName = Resolve-RgCommandName if (-not $commandName) { return @() } try { @(& $commandName @Arguments 2>$null | ForEach-Object { $_ -replace '\e\[[0-9;?]*[ -/]*[@-~]', '' }) } catch { @() } } function Get-RgQuoteChar { # PowerShell reads ' and U+2018-U+201B as single quotes and " and U+201C-U+201E as double quotes. param([string]$Value) if ([string]::IsNullOrEmpty($Value) -or $Value[0] -notin [char[]]@(0x27, 0x2018, 0x2019, 0x201A, 0x201B, 0x22, 0x201C, 0x201D, 0x201E)) { return '' } [string]$Value[0] } function Remove-RgOuterQuotes { param([string]$Value) if ($null -eq $Value) { return '' } # A quoted word is read the way PowerShell reads it, closing an open quote first, so typed # escapes ('' `" `$) are undone; anything the parser cannot reduce to a constant is trimmed. $quoteChar = Get-RgQuoteChar -Value $Value if ($quoteChar) { foreach ($candidate in @($Value, ($Value + $quoteChar))) { $tokens = $null $parseErrors = $null $ast = [System.Management.Automation.Language.Parser]::ParseInput($candidate, [ref]$tokens, [ref]$parseErrors) if ($parseErrors.Count -gt 0) { continue } $constant = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.StringConstantExpressionAst] -and $node.Extent.Text -eq $candidate }, $true) if ($constant) { return $constant.Value } } } $Value.Trim([char[]]@(0x27, 0x2018, 0x2019, 0x201A, 0x201B, 0x22, 0x201C, 0x201D, 0x201E)) } function ConvertTo-RgQuotedValue { param( [string]$Value, [string]$QuoteChar = '' ) if ([string]::IsNullOrWhiteSpace($Value)) { return $Value } if (-not $QuoteChar) { if ($Value -notmatch '[\s{}();,|&<>''"`$@#\u2018-\u201E]') { return $Value } $QuoteChar = "'" } # Keep the quote the user typed; an unquoted value that needs quoting gets single quotes. if ($QuoteChar -in [string[]]@('"', [string][char]0x201C, [string][char]0x201D, [string][char]0x201E)) { return $QuoteChar + ($Value -replace '([`"$\u201C-\u201E])', '`$1') + $QuoteChar } $QuoteChar + ($Value -replace "(['\u2018-\u201B])", '$1$1') + $QuoteChar } function Test-RgPathLikeInput { param([string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return $false } $cleanValue = Remove-RgOuterQuotes -Value $Value $cleanValue -match '^(?:\.{1,2}[\\/]|[\\/]|~[\\/]|[A-Za-z]:|\\\\)' } function Get-RgTokenText { 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-RgCurrentToken { param( [string]$Line, [int]$CursorPosition, [string]$Fallback ) if ([string]::IsNullOrWhiteSpace($Line)) { return $Fallback } $safeCursor = [Math]::Min([Math]::Max($CursorPosition, 0), $Line.Length) $prefix = $Line.Substring(0, $safeCursor) # The word is the PowerShell token that ends at the cursor, which is exactly the span PowerShell # replaces: an open quote runs to the cursor ("C:\Program Fi, --opt="a b), while a closed quoted # segment ends its token, so in "a b"c or ""x only the bare text after it is the word. $tokens = $null $parseErrors = $null [void][System.Management.Automation.Language.Parser]::ParseInput($prefix, [ref]$tokens, [ref]$parseErrors) $last = @($tokens | Where-Object { $_.Kind -ne [System.Management.Automation.Language.TokenKind]::EndOfInput }) | Select-Object -Last 1 if ($null -eq $last) { return $Fallback } if ($last.Extent.EndOffset -lt $prefix.Length) { return '' } $last.Text } function Get-RgArgumentTokens { param( [System.Management.Automation.Language.CommandAst]$CommandAst, [int]$CursorPosition ) $tokens = @() foreach ($element in $CommandAst.CommandElements | Select-Object -Skip 1) { if ($element.Extent.EndOffset -lt $CursorPosition) { $tokens += Get-RgTokenText -Element $element } } $tokens } function Get-RgValueKind { param( [string]$Token, [string]$Placeholder ) $tokenKey = if ($Token.StartsWith('--')) { $Token.ToLowerInvariant() } else { $Token } $placeholderKey = if ([string]::IsNullOrWhiteSpace($Placeholder)) { '' } else { $Placeholder.ToUpperInvariant() } switch -CaseSensitive ($tokenKey) { '-e' { return 'Pattern' } '--regexp' { return 'Pattern' } '-f' { return 'PatternFilePathOrStdin' } '--file' { return 'PatternFilePathOrStdin' } '-g' { return 'Glob' } '--glob' { return 'Glob' } '--iglob' { return 'Glob' } '--pre-glob' { return 'Glob' } '-t' { return 'FileType' } '--type' { return 'FileType' } '-T' { return 'FileType' } '--type-not' { return 'FileType' } '--type-clear' { return 'FileType' } '--type-add' { return 'TypeSpec' } '-E' { return 'Encoding' } '--encoding' { return 'Encoding' } '--engine' { return 'Engine' } '--sort' { return 'Sort' } '--sortr' { return 'Sort' } '--color' { return 'ColorWhen' } '--colors' { return 'ColorSpec' } '--generate' { return 'GenerateKind' } '--hyperlink-format' { return 'HyperlinkFormat' } '--path-separator' { return 'PathSeparator' } '--context-separator' { return 'Separator' } '--field-context-separator' { return 'Separator' } '--field-match-separator' { return 'Separator' } '--pre' { return 'Command' } '--ignore-file' { return 'FilePath' } '-r' { return 'Replacement' } '--replace' { return 'Replacement' } '-A' { return 'Number' } '--after-context' { return 'Number' } '-B' { return 'Number' } '--before-context' { return 'Number' } '-C' { return 'Number' } '--context' { return 'Number' } '-j' { return 'Number' } '--threads' { return 'Number' } '-m' { return 'Number' } '--max-count' { return 'Number' } '-M' { return 'Number' } '--max-columns' { return 'Number' } '--max-depth' { return 'Number' } '--dfa-size-limit' { return 'Size' } '--regex-size-limit' { return 'Size' } '--max-filesize' { return 'Size' } } switch ($placeholderKey) { 'PATTERN' { return 'Pattern' } 'PATTERNFILE' { return 'PatternFilePathOrStdin' } 'PATH' { return 'Path' } 'TYPE' { return 'FileType' } 'TYPESPEC' { return 'TypeSpec' } 'ENCODING' { return 'Encoding' } 'ENGINE' { return 'Engine' } 'SORTBY' { return 'Sort' } 'WHEN' { return 'ColorWhen' } 'COLOR_SPEC' { return 'ColorSpec' } 'KIND' { return 'GenerateKind' } 'FORMAT' { return 'HyperlinkFormat' } 'SEPARATOR' { return 'Separator' } 'COMMAND' { return 'Command' } 'GLOB' { return 'Glob' } 'NUM' { return 'Number' } 'NUM+SUFFIX?' { return 'Size' } } $null } function Get-RgCanonicalOptionKey { param([string]$Token) if ([string]::IsNullOrWhiteSpace($Token)) { return $null } if ($Token.StartsWith('--')) { return 'long:' + $Token.ToLowerInvariant() } if ($Token.StartsWith('-')) { return 'short:' + $Token } $null } function ConvertFrom-RgOptionSpecLine { param([string]$Line) $specLine = $Line.Trim() $parts = $specLine -split ',\s+', 2 $results = New-Object System.Collections.Generic.List[object] if ($parts.Count -eq 2) { $shortPart = $parts[0].Trim() $longPart = $parts[1].Trim() if ($shortPart -match '^(?<token>-[^,\s]+)(?:\s+(?<value>\S+))?$') { [void]$results.Add([pscustomobject]@{ Token = $matches['token'] DisplayText = $shortPart Placeholder = $matches['value'] }) } if ($longPart -match '^(?<token>--[A-Za-z0-9][A-Za-z0-9\-]*)(?:[= ](?<value>\S+))?$') { [void]$results.Add([pscustomobject]@{ Token = $matches['token'] DisplayText = $longPart Placeholder = $matches['value'] }) } } elseif ($specLine -match '^(?<token>--[A-Za-z0-9][A-Za-z0-9\-]*)(?:[= ](?<value>\S+))?$') { [void]$results.Add([pscustomobject]@{ Token = $matches['token'] DisplayText = $specLine Placeholder = $matches['value'] }) } @($results.ToArray()) } function Add-RgOptionSpec { param( [string]$Token, [string]$DisplayText, [string]$Description, [string]$Placeholder ) if ([string]::IsNullOrWhiteSpace($Token)) { return } $catalog = Get-RgCompletionCatalog $key = Get-RgCanonicalOptionKey -Token $Token if ($catalog.OptionByToken.ContainsKey($key)) { if (-not [string]::IsNullOrWhiteSpace($Description)) { $catalog.OptionByToken[$key].Description = $Description } return } $catalog.OptionByToken[$key] = [pscustomobject]@{ Token = $Token DisplayText = if ([string]::IsNullOrWhiteSpace($DisplayText)) { $Token } else { $DisplayText } Description = $Description Placeholder = $Placeholder ValueKind = Get-RgValueKind -Token $Token -Placeholder $Placeholder } $catalog.Options += $catalog.OptionByToken[$key] } function Initialize-RgCompletionCatalog { $catalog = Get-RgCompletionCatalog if ($catalog.Initialized) { return } $helpLines = Invoke-RgCapture -Arguments @('--help') $currentKeys = @() $lastOptionKey = $null foreach ($line in @($helpLines)) { if ($line -match '^\s*(?:-[^,\s]+(?:\s+\S+)?\,\s+)?--[A-Za-z0-9][A-Za-z0-9\-]*(?:[= ]\S+)?\s*$') { $currentKeys = @() foreach ($parsed in @(ConvertFrom-RgOptionSpecLine -Line $line)) { Add-RgOptionSpec -Token $parsed.Token -DisplayText $parsed.DisplayText -Description $parsed.DisplayText -Placeholder $parsed.Placeholder $currentKeys += Get-RgCanonicalOptionKey -Token $parsed.Token } if ($currentKeys.Count -gt 0) { $lastOptionKey = $currentKeys[-1] } continue } # Hidden aliases (--maxdepth for --max-depth) are documented only in the prose paragraphs. if ($lastOptionKey -and $line -match 'alternative spelling for this flag is (?<alias>--[A-Za-z0-9][A-Za-z0-9\-]*)') { $aliasToken = $matches['alias'] $primary = $catalog.OptionByToken[$lastOptionKey] Add-RgOptionSpec -Token $aliasToken -DisplayText $aliasToken -Description ('Alternative spelling of ' + $primary.Token + '.') -Placeholder $primary.Placeholder } if ($currentKeys.Count -gt 0 -and $line -match '^\s{8,}(?<text>\S.*)$') { $continuation = $matches['text'].Trim() foreach ($key in $currentKeys) { $option = $catalog.OptionByToken[$key] if ([string]::IsNullOrWhiteSpace($option.Description) -or $option.Description -eq $option.DisplayText) { $option.Description = $continuation } elseif (-not $option.Description.EndsWith($continuation, [System.StringComparison]::OrdinalIgnoreCase)) { $option.Description += ' ' + $continuation } } continue } if ([string]::IsNullOrWhiteSpace($line) -or $line -match '^\S') { $currentKeys = @() } } # ripgrep prints negation and alias spellings (--no-*, --maxdepth, --ignore, ...) only in # prose, so harvest the remaining accepted flags from its own generated completion script. $generated = @(Invoke-RgCapture -Arguments @('--generate', 'complete-powershell')) foreach ($line in $generated) { if ($line -match "\[CompletionResult\]::new\('(?<token>--?[A-Za-z0-9][A-Za-z0-9\-]*)',\s*'[^']*',\s*\[CompletionResultType\]::ParameterName,\s*'(?<desc>(?:[^']|'')*)'\)") { $description = $matches['desc'].Replace("''", "'") $key = Get-RgCanonicalOptionKey -Token $matches['token'] if (-not $catalog.OptionByToken.ContainsKey($key)) { Add-RgOptionSpec -Token $matches['token'] -DisplayText $matches['token'] -Description $description } } } $catalog.TypeNames = Get-RgUniqueStrings -Items ( Invoke-RgCapture -Arguments @('--type-list') | ForEach-Object { if ($_ -match '^\s*([A-Za-z0-9][A-Za-z0-9+-]*)\s*:') { $matches[1] } } ) $catalog.Initialized = $true } function Get-RgPathCompletions { param( [string]$InputPath, [string]$Prefix = '', [switch]$DirectoriesOnly ) $quoteChar = Get-RgQuoteChar -Value $InputPath $cleanInput = if ([string]::IsNullOrWhiteSpace($InputPath)) { '' } else { Remove-RgOuterQuotes -Value $InputPath } # Candidates keep the typed directory text (.\, ./, ..\, ~\, C:\...) exactly as typed. $separatorIndex = $cleanInput.LastIndexOfAny([char[]]@('\', '/')) $typedDirectory = $cleanInput.Substring(0, $separatorIndex + 1) $leaf = $cleanInput.Substring($separatorIndex + 1) $parent = if ($typedDirectory) { $typedDirectory } else { '.' } $separator = if ($typedDirectory.EndsWith('/')) { '/' } else { [string][System.IO.Path]::DirectorySeparatorChar } $filter = if ([string]::IsNullOrWhiteSpace($leaf)) { '*' } else { "$leaf*" } $items = @(Get-ChildItem -Path $parent -Filter $filter -ErrorAction Ignore) if ($DirectoriesOnly) { $items = @($items | Where-Object { $_.PSIsContainer }) } foreach ($item in $items) { $completionText = $typedDirectory + $item.Name if ($item.PSIsContainer) { $completionText += $separator } # A whole-word value starting with a dash would parse as a parameter: anchor it to the current directory. if (-not $Prefix -and $completionText -match '^[-\u2013-\u2015]') { $completionText = '.' + $separator + $completionText } $completionText = ConvertTo-RgQuotedValue -Value $completionText -QuoteChar $quoteChar $completionText = $Prefix + $completionText New-RgCompletionResult -CompletionText $completionText -ListItemText $item.Name -ResultType 'ParameterValue' -ToolTip $item.FullName } } function New-RgLiteralValueResults { param( [string]$CurrentValue, [string]$Placeholder, [string]$ToolTip, [string]$Prefix = '' ) if ([string]::IsNullOrWhiteSpace($CurrentValue)) { return @( New-RgCompletionResult -CompletionText ($Prefix + $Placeholder) -ListItemText $Placeholder -ResultType 'ParameterValue' -ToolTip $ToolTip ) } @( New-RgCompletionResult -CompletionText ($Prefix + $CurrentValue) -ListItemText $CurrentValue -ResultType 'ParameterValue' -ToolTip $ToolTip ) } function Get-RgEnumValueResults { param( [string[]]$Values, [string]$CurrentValue, [string]$ToolTip, [string]$Prefix = '' ) $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } foreach ($value in @($Values)) { if ($value -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*')) { New-RgCompletionResult -CompletionText ($Prefix + $value) -ResultType 'ParameterValue' -ToolTip $ToolTip } } } function Get-RgEnumOrLiteralValueResults { param( [string[]]$Values, [string]$CurrentValue, [string]$Placeholder, [string]$ToolTip, [string]$Prefix = '' ) $results = @(Get-RgEnumValueResults -Values $Values -CurrentValue $CurrentValue -ToolTip $ToolTip -Prefix $Prefix) if ($results.Count -gt 0) { return $results } New-RgLiteralValueResults -CurrentValue $CurrentValue -Placeholder $Placeholder -ToolTip $ToolTip -Prefix $Prefix } function Get-RgPathValueResults { param( [string]$CurrentValue, [string]$Placeholder, [string]$ToolTip, [string]$Prefix = '', [switch]$DirectoriesOnly, [switch]$AllowStdinSentinel ) $results = New-Object System.Collections.Generic.List[object] $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } if ($AllowStdinSentinel -and ('-' -like ([System.Management.Automation.WildcardPattern]::Escape($typedValue) + '*'))) { [void]$results.Add((New-RgCompletionResult -CompletionText ($Prefix + '-') -ResultType 'ParameterValue' -ToolTip 'Read this value from standard input.')) } foreach ($item in @(Get-RgPathCompletions -InputPath $typedValue -Prefix $Prefix -DirectoriesOnly:$DirectoriesOnly)) { [void]$results.Add($item) } if ($results.Count -gt 0) { return @($results.ToArray()) } New-RgLiteralValueResults -CurrentValue $typedValue -Placeholder $Placeholder -ToolTip $ToolTip -Prefix $Prefix } function Get-RgTypeValueResults { param( [string]$CurrentValue, [string]$Placeholder, [string]$ToolTip, [string]$Prefix = '' ) $catalog = Get-RgCompletionCatalog $results = @(Get-RgEnumValueResults -Values $catalog.TypeNames -CurrentValue $CurrentValue -ToolTip $ToolTip -Prefix $Prefix) if ($results.Count -gt 0) { return $results } New-RgLiteralValueResults -CurrentValue $CurrentValue -Placeholder $Placeholder -ToolTip $ToolTip -Prefix $Prefix } function Get-RgTypeSpecValueResults { param( [string]$CurrentValue, [string]$ToolTip, [string]$Prefix = '' ) $catalog = Get-RgCompletionCatalog $results = New-Object System.Collections.Generic.List[object] foreach ($typeName in @($catalog.TypeNames | Select-Object -First 12)) { foreach ($candidate in @("${typeName}:*.ext", "${typeName}:include:cpp,py,md")) { if (-not [string]::IsNullOrWhiteSpace($CurrentValue) -and $candidate -notlike "$CurrentValue*") { continue } [void]$results.Add((New-RgCompletionResult -CompletionText ($Prefix + $candidate) -ResultType 'ParameterValue' -ToolTip $ToolTip)) } } if ($results.Count -gt 0) { return @($results.ToArray()) } New-RgLiteralValueResults -CurrentValue $CurrentValue -Placeholder '<type:glob>' -ToolTip $ToolTip -Prefix $Prefix } function Get-RgValueCompletions { param( [pscustomobject]$OptionSpec, [string]$CurrentValue, [string]$Prefix = '' ) $catalog = Get-RgCompletionCatalog $typedValue = if ($null -eq $CurrentValue) { '' } else { $CurrentValue } $toolTip = if ([string]::IsNullOrWhiteSpace($OptionSpec.Description)) { $OptionSpec.DisplayText } else { $OptionSpec.Description } switch ($OptionSpec.ValueKind) { 'Pattern' { return New-RgLiteralValueResults -CurrentValue $typedValue -Placeholder '<pattern>' -ToolTip $toolTip -Prefix $Prefix } 'Replacement' { return New-RgLiteralValueResults -CurrentValue $typedValue -Placeholder '<replacement>' -ToolTip $toolTip -Prefix $Prefix } 'Glob' { return New-RgLiteralValueResults -CurrentValue $typedValue -Placeholder '<glob>' -ToolTip $toolTip -Prefix $Prefix } 'PatternFilePathOrStdin' { return @(Get-RgPathValueResults -CurrentValue $typedValue -Placeholder '<pattern-file>' -ToolTip $toolTip -Prefix $Prefix -AllowStdinSentinel) } 'FilePath' { return @(Get-RgPathValueResults -CurrentValue $typedValue -Placeholder '<file>' -ToolTip $toolTip -Prefix $Prefix) } 'Path' { return @(Get-RgPathValueResults -CurrentValue $typedValue -Placeholder '<path>' -ToolTip $toolTip -Prefix $Prefix) } 'FileType' { return @(Get-RgTypeValueResults -CurrentValue $typedValue -Placeholder '<type>' -ToolTip $toolTip -Prefix $Prefix) } 'TypeSpec' { return @(Get-RgTypeSpecValueResults -CurrentValue $typedValue -ToolTip $toolTip -Prefix $Prefix) } 'Encoding' { return @(Get-RgEnumOrLiteralValueResults -Values $catalog.EncodingSuggestions -CurrentValue $typedValue -Placeholder '<encoding>' -ToolTip $toolTip -Prefix $Prefix) } 'Engine' { return @(Get-RgEnumOrLiteralValueResults -Values @('default', 'pcre2', 'auto') -CurrentValue $typedValue -Placeholder '<engine>' -ToolTip $toolTip -Prefix $Prefix) } 'Sort' { return @(Get-RgEnumOrLiteralValueResults -Values $catalog.SortValues -CurrentValue $typedValue -Placeholder '<sort>' -ToolTip $toolTip -Prefix $Prefix) } 'ColorWhen' { return @(Get-RgEnumOrLiteralValueResults -Values $catalog.ColorWhenValues -CurrentValue $typedValue -Placeholder '<when>' -ToolTip $toolTip -Prefix $Prefix) } 'ColorSpec' { return @(Get-RgEnumOrLiteralValueResults -Values $catalog.ColorSpecSuggestions -CurrentValue $typedValue -Placeholder '<color-spec>' -ToolTip $toolTip -Prefix $Prefix) } 'GenerateKind' { return @(Get-RgEnumOrLiteralValueResults -Values $catalog.GenerateKinds -CurrentValue $typedValue -Placeholder '<kind>' -ToolTip $toolTip -Prefix $Prefix) } 'HyperlinkFormat' { return @(Get-RgEnumOrLiteralValueResults -Values $catalog.HyperlinkFormats -CurrentValue $typedValue -Placeholder '<format>' -ToolTip $toolTip -Prefix $Prefix) } 'PathSeparator' { return @(Get-RgEnumOrLiteralValueResults -Values @('/', '\') -CurrentValue $typedValue -Placeholder '<separator>' -ToolTip $toolTip -Prefix $Prefix) } 'Separator' { return New-RgLiteralValueResults -CurrentValue $typedValue -Placeholder '<separator>' -ToolTip $toolTip -Prefix $Prefix } 'Command' { return New-RgLiteralValueResults -CurrentValue $typedValue -Placeholder '<command>' -ToolTip $toolTip -Prefix $Prefix } 'Number' { return @(Get-RgEnumOrLiteralValueResults -Values @('0', '1', '2', '4', '8', '16', '32', '64', '120') -CurrentValue $typedValue -Placeholder '<number>' -ToolTip $toolTip -Prefix $Prefix) } 'Size' { return @(Get-RgEnumOrLiteralValueResults -Values @('64K', '1M', '10M', '100M', '1G') -CurrentValue $typedValue -Placeholder '<size>' -ToolTip $toolTip -Prefix $Prefix) } default { return New-RgLiteralValueResults -CurrentValue $typedValue -Placeholder '<value>' -ToolTip $toolTip -Prefix $Prefix } } } function Get-RgOptionKey { param([string]$Token) $cleanToken = Remove-RgOuterQuotes -Value $Token if ($cleanToken -match '^(--[A-Za-z0-9][A-Za-z0-9\-]*)') { return Get-RgCanonicalOptionKey -Token $matches[1] } if ($cleanToken -match '^(-[^=\s]+)$') { return Get-RgCanonicalOptionKey -Token $matches[1] } $null } function Resolve-RgShortToken { # Decomposes a single-dash token into known short flags. A value-taking flag ends the # cluster and the remainder of the token is its attached value (-tpy, -A5, -iA5). param([string]$Token) $cleanToken = Remove-RgOuterQuotes -Value $Token if ($cleanToken.Length -lt 2 -or -not $cleanToken.StartsWith('-') -or $cleanToken.StartsWith('--')) { return $null } $catalog = Get-RgCompletionCatalog $flags = New-Object System.Collections.Generic.List[object] $valueOption = $null $value = '' $prefix = '-' for ($index = 1; $index -lt $cleanToken.Length; $index++) { $key = Get-RgCanonicalOptionKey -Token ('-' + $cleanToken[$index]) if (-not $catalog.OptionByToken.ContainsKey($key)) { return $null } $option = $catalog.OptionByToken[$key] $flags.Add($option) $prefix += $cleanToken[$index] if ($option.ValueKind) { $valueOption = $option $value = $cleanToken.Substring($index + 1) break } } [pscustomobject]@{ Flags = @($flags.ToArray()) ValueOption = $valueOption Value = $value Prefix = $prefix } } function Get-RgShortClusterCompletion { param([pscustomobject]$Cluster) $catalog = Get-RgCompletionCatalog $present = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) foreach ($flag in $Cluster.Flags) { [void]$present.Add($flag.Token) } New-RgCompletionResult -CompletionText $Cluster.Prefix -ResultType 'ParameterName' -ToolTip ('Flags: ' + (($Cluster.Flags | ForEach-Object { $_.Token }) -join ' ')) foreach ($option in $catalog.Options) { if ($option.Token.Length -ne 2 -or $option.Token.StartsWith('--') -or $present.Contains($option.Token)) { continue } $toolTip = if ([string]::IsNullOrWhiteSpace($option.Description)) { $option.DisplayText } else { $option.Description } New-RgCompletionResult -CompletionText ($Cluster.Prefix + $option.Token.Substring(1)) -ListItemText ($Cluster.Prefix + $option.Token.Substring(1)) -ResultType 'ParameterName' -ToolTip $toolTip } } function Get-RgCompletionContext { param([string[]]$TokensBeforeCurrent) Initialize-RgCompletionCatalog $catalog = Get-RgCompletionCatalog $positionals = New-Object System.Collections.Generic.List[string] $pendingOption = $null $endOfOptions = $false $filesMode = $false $hasPatternSource = $false $terminalMode = $false foreach ($token in @($TokensBeforeCurrent)) { $cleanToken = Remove-RgOuterQuotes -Value $token if ([string]::IsNullOrWhiteSpace($cleanToken)) { continue } if ($pendingOption) { if ($pendingOption.ValueKind -in @('Pattern', 'PatternFilePathOrStdin')) { $hasPatternSource = $true } if ($pendingOption.Token -eq '--generate') { $terminalMode = $true } $pendingOption = $null continue } if ($endOfOptions) { $positionals.Add($cleanToken) continue } if ($cleanToken -eq '--') { $endOfOptions = $true continue } $cluster = if ($cleanToken.Length -gt 2) { Resolve-RgShortToken -Token $cleanToken } else { $null } if ($cluster) { foreach ($flag in $cluster.Flags) { if ($flag.Token -in @('-h', '-V')) { $terminalMode = $true } } if ($cluster.ValueOption) { if ($cluster.Value.Length -gt 0) { if ($cluster.ValueOption.ValueKind -in @('Pattern', 'PatternFilePathOrStdin')) { $hasPatternSource = $true } } else { $pendingOption = $cluster.ValueOption } } continue } if ($cleanToken -match '^(--[^=]+)=(.*)$') { $lookup = Get-RgCanonicalOptionKey -Token $matches[1] if ($catalog.OptionByToken.ContainsKey($lookup)) { $option = $catalog.OptionByToken[$lookup] if ($option.ValueKind -in @('Pattern', 'PatternFilePathOrStdin')) { $hasPatternSource = $true } if ($option.Token -eq '--generate') { $terminalMode = $true } continue } } $lookup = Get-RgOptionKey -Token $cleanToken if ($lookup -and $catalog.OptionByToken.ContainsKey($lookup)) { $option = $catalog.OptionByToken[$lookup] if ($option.Token -eq '--files') { $filesMode = $true } if ($option.Token -in @('-h', '--help', '-V', '--version', '--pcre2-version', '--type-list')) { $terminalMode = $true } if ($option.ValueKind) { $pendingOption = $option } continue } $positionals.Add($cleanToken) } [pscustomobject]@{ PendingOption = $pendingOption EndOfOptions = $endOfOptions FilesMode = $filesMode HasPatternSource = $hasPatternSource TerminalMode = $terminalMode Positionals = @($positionals) } } function Get-RgOptionCompletions { param([string]$CurrentWord) Initialize-RgCompletionCatalog $catalog = Get-RgCompletionCatalog $cleanCurrent = Remove-RgOuterQuotes -Value $CurrentWord foreach ($option in $catalog.Options) { $matchesPrefix = $false if ([string]::IsNullOrWhiteSpace($cleanCurrent)) { $matchesPrefix = $true } elseif ($cleanCurrent.StartsWith('--')) { $matchesPrefix = $option.Token.StartsWith($cleanCurrent, [System.StringComparison]::OrdinalIgnoreCase) } else { $matchesPrefix = $option.Token.StartsWith($cleanCurrent, [System.StringComparison]::Ordinal) } if (-not $matchesPrefix) { continue } $toolTip = if ([string]::IsNullOrWhiteSpace($option.Description)) { $option.DisplayText } else { $option.Description } New-RgCompletionResult -CompletionText $option.Token -ListItemText $option.DisplayText -ResultType 'ParameterName' -ToolTip $toolTip } } function Get-RgPositionalCompletions { param( [string]$CurrentWord, [pscustomobject]$Context ) if ($Context.TerminalMode -and -not $Context.FilesMode) { return @() } if ($Context.FilesMode -or $Context.HasPatternSource -or $Context.Positionals.Count -gt 0) { return @(Get-RgPathValueResults -CurrentValue $CurrentWord -Placeholder '<path>' -ToolTip 'File or directory path to search.') } New-RgLiteralValueResults -CurrentValue $CurrentWord -Placeholder '<pattern>' -ToolTip 'Regular expression or literal pattern to search.' } Register-ArgumentCompleter -Native -CommandName 'rg', 'rg.exe' -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) if ($wordToComplete -isnot [string]) { $wordToComplete = [string]$wordToComplete } Initialize-RgCompletionCatalog $catalog = Get-RgCompletionCatalog # The AST extent stops at the last token, so an empty $wordToComplete is the only reliable # signal that the cursor sits after whitespace and a fresh slot is being completed. $currentToken = if ([string]::IsNullOrEmpty($wordToComplete)) { '' } else { Get-RgCurrentToken -Line $commandAst.Extent.Text -CursorPosition ($cursorPosition - $commandAst.Extent.StartOffset) -Fallback $wordToComplete } $tokensBeforeCurrent = Get-RgArgumentTokens -CommandAst $commandAst -CursorPosition $cursorPosition $context = Get-RgCompletionContext -TokensBeforeCurrent $tokensBeforeCurrent if ($currentToken -match '^(--[^=]+)=(.*)$') { $optionKey = Get-RgCanonicalOptionKey -Token $matches[1] $valuePrefix = $matches[2] if ($catalog.OptionByToken.ContainsKey($optionKey)) { $optionSpec = $catalog.OptionByToken[$optionKey] if ($optionSpec.ValueKind) { return @(Get-RgValueCompletions -OptionSpec $optionSpec -CurrentValue $valuePrefix -Prefix ($matches[1] + '=')) } } } if ($currentToken.Length -gt 2 -and -not $context.EndOfOptions) { $cluster = Resolve-RgShortToken -Token $currentToken if ($cluster) { if ($cluster.ValueOption) { return @(Get-RgValueCompletions -OptionSpec $cluster.ValueOption -CurrentValue $cluster.Value -Prefix $cluster.Prefix) } return @(Get-RgShortClusterCompletion -Cluster $cluster) } } if ($context.PendingOption) { return @(Get-RgValueCompletions -OptionSpec $context.PendingOption -CurrentValue $currentToken) } if ($currentToken.StartsWith('-') -and -not $context.EndOfOptions) { return @(Get-RgOptionCompletions -CurrentWord $currentToken) } @(Get-RgPositionalCompletions -CurrentWord $currentToken -Context $context) } |