completers/wt_completer/wt_completer.ps1
|
# PowerShell Argument Completer for wt.exe # Provides tab completion for top-level wt options, subcommands, and selected subcommand arguments. Set-StrictMode -Version Latest function Get-WtSettingsNames { param([string]$Kind) $cache = Get-Variable -Name 'WtSettingsNames' -Scope Script -ErrorAction Ignore if ($null -eq $cache -or $null -eq $cache.Value) { # Built-in schemes shipped in the package's defaults.json (1.25/1.26); they never # appear in the user's settings.json 'schemes' array. $names = @{ profiles = @() schemes = @( 'Campbell', 'Campbell Powershell', 'Vintage', 'One Half Dark', 'One Half Light', 'Solarized Dark', 'Solarized Light', 'Tango Dark', 'Tango Light', 'Dark+', 'VSCode Dark Modern', 'VSCode Light Modern', 'CGA', 'IBM 5153', 'Dimidium', 'Ottosson' ) } foreach ($package in 'Microsoft.WindowsTerminal_8wekyb3d8bbwe', 'Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe') { $settingsPath = Join-Path -Path $env:LOCALAPPDATA -ChildPath "Packages\$package\LocalState\settings.json" if (-not (Test-Path -LiteralPath $settingsPath)) { continue } try { $settings = Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json } catch { continue } if ($settings.PSObject.Properties['profiles'] -and $settings.profiles.PSObject.Properties['list']) { $names.profiles += @($settings.profiles.list | ForEach-Object { $_.name } | Where-Object { $_ }) } if ($settings.PSObject.Properties['schemes']) { $names.schemes += @($settings.schemes | ForEach-Object { $_.name } | Where-Object { $_ }) } # Schemes referenced by profiles: a plain name or a { light, dark } pair. if ($settings.PSObject.Properties['profiles']) { $profileEntries = @() if ($settings.profiles.PSObject.Properties['defaults']) { $profileEntries += $settings.profiles.defaults } if ($settings.profiles.PSObject.Properties['list']) { $profileEntries += @($settings.profiles.list) } foreach ($entry in $profileEntries) { if ($null -eq $entry -or -not $entry.PSObject.Properties['colorScheme']) { continue } $scheme = $entry.colorScheme if ($scheme -is [string]) { $names.schemes += $scheme } elseif ($null -ne $scheme) { foreach ($variant in 'light', 'dark') { if ($scheme.PSObject.Properties[$variant] -and $scheme.$variant -is [string]) { $names.schemes += $scheme.$variant } } } } } } $names.profiles = @($names.profiles | Sort-Object -Unique) $names.schemes = @($names.schemes | Where-Object { $_ } | Sort-Object -Unique) Set-Variable -Name 'WtSettingsNames' -Value $names -Scope Script $cache = Get-Variable -Name 'WtSettingsNames' -Scope Script } @($cache.Value[$Kind]) } function Get-WtValueCompletionData { param([string]$Option) if ($Option -cin '-w', '--window') { # Reserved window ids; any other integer id or window name is also accepted. return @( @{ Text = 'new'; Display = 'new'; Type = 'ParameterValue'; Tooltip = 'Always run this command in a new window' } @{ Text = 'last'; Display = 'last'; Type = 'ParameterValue'; Tooltip = 'Always run this command in the most recently used window' } @{ Text = '-1'; Display = '-1'; Type = 'ParameterValue'; Tooltip = 'Always run this command in a new window (same as new)' } @{ Text = '0'; Display = '0'; Type = 'ParameterValue'; Tooltip = 'Always run this command in the most recently used window (same as last)' } ) } $kind = if ($Option -in '-p', '--profile') { 'profiles' } elseif ($Option -eq '--colorScheme') { 'schemes' } else { $null } if (-not $kind) { return @() } @( foreach ($name in Get-WtSettingsNames -Kind $kind) { @{ Text = $name; Display = $name; Type = 'ParameterValue'; Tooltip = "Windows Terminal $kind entry" } } ) } function ConvertTo-WtQuotedValue { param([string]$Value, [string]$Quote) if ($Quote -eq '"') { return '"' + $Value.Replace('`', '``').Replace('"', '`"').Replace('$', '`$') + '"' } "'" + $Value.Replace("'", "''") + "'" } function Get-WtUnquotedWord { param([string]$Text) # An unterminated quote runs to the cursor; strip the opening quote (and a matching # closing one) and report which quote character the user typed. if ($Text.Length -gt 0 -and ($Text[0] -eq [char]"'" -or $Text[0] -eq [char]'"')) { $quote = [string]$Text[0] $inner = $Text.Substring(1) if ($inner.Length -gt 0 -and $inner.EndsWith($quote)) { $inner = $inner.Substring(0, $inner.Length - 1) } return @{ Word = $inner; Quote = $quote } } @{ Word = $Text; Quote = '' } } function Complete-WtNative { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'wordToComplete', Justification = 'The word is cut from the CommandAst element at the cursor; wordToComplete drops quotes and ignores the cursor.')] param($wordToComplete, $commandAst, $cursorPosition) if (-not (Get-Command wt.exe -ErrorAction Ignore) -and -not (Get-Command wt -ErrorAction Ignore)) { return } $topLevelOptionData = @( @{ Text = '-h'; Display = '-h'; Type = 'ParameterName'; Tooltip = 'Print this help message and exit' } @{ Text = '--help'; Display = '--help'; Type = 'ParameterName'; Tooltip = 'Print this help message and exit' } @{ Text = '-v'; Display = '-v'; Type = 'ParameterName'; Tooltip = 'Display the application version' } @{ Text = '--version'; Display = '--version'; Type = 'ParameterName'; Tooltip = 'Display the application version' } @{ Text = '-M'; Display = '-M'; Type = 'ParameterName'; Tooltip = 'Launch the window maximized' } @{ Text = '--maximized'; Display = '--maximized'; Type = 'ParameterName'; Tooltip = 'Launch the window maximized' } @{ Text = '-F'; Display = '-F'; Type = 'ParameterName'; Tooltip = 'Launch the window in fullscreen mode' } @{ Text = '--fullscreen'; Display = '--fullscreen'; Type = 'ParameterName'; Tooltip = 'Launch the window in fullscreen mode' } @{ Text = '-f'; Display = '-f'; Type = 'ParameterName'; Tooltip = 'Launch the window in focus mode' } @{ Text = '--focus'; Display = '--focus'; Type = 'ParameterName'; Tooltip = 'Launch the window in focus mode' } @{ Text = '--pos'; Display = '--pos'; Type = 'ParameterName'; Tooltip = 'Specify the position for the terminal, in ''x,y'' format' } @{ Text = '--size'; Display = '--size'; Type = 'ParameterName'; Tooltip = 'Specify the number of columns and rows for the terminal, in ''c,r'' format' } @{ Text = '-w'; Display = '-w'; Type = 'ParameterName'; Tooltip = 'Specify a terminal window to run the given commandline in' } @{ Text = '--window'; Display = '--window'; Type = 'ParameterName'; Tooltip = 'Specify a terminal window to run the given commandline in' } @{ Text = '-s'; Display = '-s'; Type = 'ParameterName'; Tooltip = 'Internal parameter for saved command lines' } @{ Text = '--saved'; Display = '--saved'; Type = 'ParameterName'; Tooltip = 'Internal parameter for saved command lines' } ) $newTerminalOptionData = @( @{ Text = '-p'; Display = '-p'; Type = 'ParameterName'; Tooltip = 'Use the specified profile' } @{ Text = '--profile'; Display = '--profile'; Type = 'ParameterName'; Tooltip = 'Use the specified profile' } @{ Text = '--sessionId'; Display = '--sessionId'; Type = 'ParameterName'; Tooltip = 'Reuse or assign the specified session ID' } @{ Text = '-d'; Display = '-d'; Type = 'ParameterName'; Tooltip = 'Set the starting directory' } @{ Text = '--startingDirectory'; Display = '--startingDirectory'; Type = 'ParameterName'; Tooltip = 'Set the starting directory' } @{ Text = '--title'; Display = '--title'; Type = 'ParameterName'; Tooltip = 'Set the starting tab title' } @{ Text = '--tabColor'; Display = '--tabColor'; Type = 'ParameterName'; Tooltip = 'Set the starting tab color' } @{ Text = '--suppressApplicationTitle'; Display = '--suppressApplicationTitle'; Type = 'ParameterName'; Tooltip = 'Prevent the terminal application from changing the tab title' } @{ Text = '--useApplicationTitle'; Display = '--useApplicationTitle'; Type = 'ParameterName'; Tooltip = 'Allow the terminal application to change the tab title' } @{ Text = '--colorScheme'; Display = '--colorScheme'; Type = 'ParameterName'; Tooltip = 'Set the color scheme' } @{ Text = '--appendCommandLine'; Display = '--appendCommandLine'; Type = 'ParameterName'; Tooltip = 'Append the provided command line to the profile command line' } @{ Text = '--inheritEnvironment'; Display = '--inheritEnvironment'; Type = 'ParameterName'; Tooltip = 'Inherit environment variables into the new terminal' } @{ Text = '--reloadEnvironment'; Display = '--reloadEnvironment'; Type = 'ParameterName'; Tooltip = 'Reload the environment instead of inheriting it' } ) $splitPaneOptionData = @( @{ Text = '-H'; Display = '-H'; Type = 'ParameterName'; Tooltip = 'Split horizontally' } @{ Text = '--horizontal'; Display = '--horizontal'; Type = 'ParameterName'; Tooltip = 'Split horizontally' } @{ Text = '-V'; Display = '-V'; Type = 'ParameterName'; Tooltip = 'Split vertically' } @{ Text = '--vertical'; Display = '--vertical'; Type = 'ParameterName'; Tooltip = 'Split vertically' } @{ Text = '-s'; Display = '-s'; Type = 'ParameterName'; Tooltip = 'Set the pane size ratio between 0.01 and 0.99' } @{ Text = '--size'; Display = '--size'; Type = 'ParameterName'; Tooltip = 'Set the pane size ratio between 0.01 and 0.99' } @{ Text = '-D'; Display = '-D'; Type = 'ParameterName'; Tooltip = 'Duplicate the focused pane instead of launching a new terminal' } @{ Text = '--duplicate'; Display = '--duplicate'; Type = 'ParameterName'; Tooltip = 'Duplicate the focused pane instead of launching a new terminal' } ) $focusTabOptionData = @( @{ Text = '-t'; Display = '-t'; Type = 'ParameterName'; Tooltip = 'Target the specified tab index' } @{ Text = '--target'; Display = '--target'; Type = 'ParameterName'; Tooltip = 'Target the specified tab index' } @{ Text = '-n'; Display = '-n'; Type = 'ParameterName'; Tooltip = 'Move focus to the next tab' } @{ Text = '--next'; Display = '--next'; Type = 'ParameterName'; Tooltip = 'Move focus to the next tab' } @{ Text = '-p'; Display = '-p'; Type = 'ParameterName'; Tooltip = 'Move focus to the previous tab' } @{ Text = '--previous'; Display = '--previous'; Type = 'ParameterName'; Tooltip = 'Move focus to the previous tab' } ) $movePaneOptionData = @( @{ Text = '-t'; Display = '-t'; Type = 'ParameterName'; Tooltip = 'Move the focused pane to the specified tab index' } @{ Text = '--tab'; Display = '--tab'; Type = 'ParameterName'; Tooltip = 'Move the focused pane to the specified tab index' } ) $focusPaneOptionData = @( @{ Text = '-t'; Display = '-t'; Type = 'ParameterName'; Tooltip = 'Focus the specified pane index' } @{ Text = '--target'; Display = '--target'; Type = 'ParameterName'; Tooltip = 'Focus the specified pane index' } ) $directionValueData = @( @{ Text = 'left'; Display = 'left'; Type = 'ParameterValue'; Tooltip = 'Direction: left' } @{ Text = 'right'; Display = 'right'; Type = 'ParameterValue'; Tooltip = 'Direction: right' } @{ Text = 'up'; Display = 'up'; Type = 'ParameterValue'; Tooltip = 'Direction: up' } @{ Text = 'down'; Display = 'down'; Type = 'ParameterValue'; Tooltip = 'Direction: down' } @{ Text = 'previous'; Display = 'previous'; Type = 'ParameterValue'; Tooltip = 'Direction: previous focused pane' } @{ Text = 'nextInOrder'; Display = 'nextInOrder'; Type = 'ParameterValue'; Tooltip = 'Direction: next pane in order' } @{ Text = 'previousInOrder'; Display = 'previousInOrder'; Type = 'ParameterValue'; Tooltip = 'Direction: previous pane in order' } @{ Text = 'first'; Display = 'first'; Type = 'ParameterValue'; Tooltip = 'Direction: first pane' } ) $subcommandData = @( @{ Text = 'new-tab'; Display = 'new-tab'; Type = 'ParameterValue'; Tooltip = 'Create a new tab' } @{ Text = 'nt'; Display = 'nt'; Type = 'ParameterValue'; Tooltip = 'Alias for new-tab' } @{ Text = 'split-pane'; Display = 'split-pane'; Type = 'ParameterValue'; Tooltip = 'Create a new split pane' } @{ Text = 'sp'; Display = 'sp'; Type = 'ParameterValue'; Tooltip = 'Alias for split-pane' } @{ Text = 'focus-tab'; Display = 'focus-tab'; Type = 'ParameterValue'; Tooltip = 'Move focus to another tab' } @{ Text = 'ft'; Display = 'ft'; Type = 'ParameterValue'; Tooltip = 'Alias for focus-tab' } @{ Text = 'move-focus'; Display = 'move-focus'; Type = 'ParameterValue'; Tooltip = 'Move focus to the adjacent pane in the specified direction' } @{ Text = 'mf'; Display = 'mf'; Type = 'ParameterValue'; Tooltip = 'Alias for move-focus' } @{ Text = 'move-pane'; Display = 'move-pane'; Type = 'ParameterValue'; Tooltip = 'Move focused pane to another tab' } @{ Text = 'mp'; Display = 'mp'; Type = 'ParameterValue'; Tooltip = 'Alias for move-pane' } @{ Text = 'swap-pane'; Display = 'swap-pane'; Type = 'ParameterValue'; Tooltip = 'Swap the focused pane with the adjacent pane in the specified direction' } @{ Text = 'focus-pane'; Display = 'focus-pane'; Type = 'ParameterValue'; Tooltip = 'Move focus to another pane' } @{ Text = 'fp'; Display = 'fp'; Type = 'ParameterValue'; Tooltip = 'Alias for focus-pane' } @{ Text = 'x-save'; Display = 'x-save'; Type = 'ParameterValue'; Tooltip = 'Save command line as input action' } ) $subcommandMap = @{ 'new-tab' = 'new-tab' 'nt' = 'new-tab' 'split-pane' = 'split-pane' 'sp' = 'split-pane' 'focus-tab' = 'focus-tab' 'ft' = 'focus-tab' 'move-focus' = 'move-focus' 'mf' = 'move-focus' 'move-pane' = 'move-pane' 'mp' = 'move-pane' 'swap-pane' = 'swap-pane' 'focus-pane' = 'focus-pane' 'fp' = 'focus-pane' 'x-save' = 'x-save' } $topLevelValueOptions = @('--pos', '--size', '-w', '--window', '-s', '--saved') $newTerminalValueOptions = @('-p', '--profile', '--sessionId', '-d', '--startingDirectory', '--title', '--tabColor', '--colorScheme') $splitPaneValueOptions = $newTerminalValueOptions + @('-s', '--size') $focusTabValueOptions = @('-t', '--target') $movePaneValueOptions = @('-t', '--tab') $focusPaneValueOptions = @('-t', '--target') $subcommandOptionData = @{ 'new-tab' = $newTerminalOptionData 'split-pane' = $newTerminalOptionData + $splitPaneOptionData 'focus-tab' = $focusTabOptionData 'move-focus' = @() 'move-pane' = $movePaneOptionData 'swap-pane' = @() 'focus-pane' = $focusPaneOptionData 'x-save' = @() } $subcommandValueOptions = @{ 'new-tab' = $newTerminalValueOptions 'split-pane' = $splitPaneValueOptions 'focus-tab' = $focusTabValueOptions 'move-focus' = @() 'move-pane' = $movePaneValueOptions 'swap-pane' = @() 'focus-pane' = $focusPaneValueOptions 'x-save' = @() } # Read words from the parser: quoted values stay one element, and the element that # contains the cursor (cut at the cursor) is the word being completed. $currentText = '' $argumentTokens = New-Object System.Collections.Generic.List[string] foreach ($element in @($commandAst.CommandElements | Select-Object -Skip 1)) { $start = $element.Extent.StartOffset if ($start -gt $cursorPosition) { break } if ($element.Extent.EndOffset -ge $cursorPosition) { $currentText = $element.Extent.Text.Substring(0, $cursorPosition - $start) break } $argumentTokens.Add($(if ($element -is [System.Management.Automation.Language.StringConstantExpressionAst]) { $element.Value } else { $element.Extent.Text })) } $currentWord = Get-WtUnquotedWord -Text $currentText $matchPrefix = $currentWord.Word $outerQuote = $currentWord.Quote $previousToken = if ($argumentTokens.Count -gt 0) { $argumentTokens[$argumentTokens.Count - 1] } else { '' } $expandedTokens = New-Object System.Collections.Generic.List[string] foreach ($token in $argumentTokens) { if ($token -eq ';' -or $token -eq '`;') { $expandedTokens.Add(';') continue } if ($token -match '^(.+?)`?;$') { $expandedTokens.Add($Matches[1]) $expandedTokens.Add(';') continue } $expandedTokens.Add($token) } $completedTokens = @($expandedTokens.ToArray()) $selectedSubcommand = $null $expectingValueOption = $null # new-tab/split-pane (and the implicit top-level new-tab) pass everything from the first # positional up to the next ';' to the child commandline. $inCommandline = $false foreach ($token in $completedTokens) { if ($token -eq ';') { $selectedSubcommand = $null $expectingValueOption = $null $inCommandline = $false continue } if ($inCommandline) { continue } if ($expectingValueOption) { $expectingValueOption = $null continue } if (-not $selectedSubcommand) { if (($topLevelValueOptions + $newTerminalValueOptions) -ccontains $token) { $expectingValueOption = $token continue } if ($subcommandMap.ContainsKey($token)) { $selectedSubcommand = $subcommandMap[$token] continue } if (-not $token.StartsWith('-')) { $inCommandline = $true } continue } $isNewTerminal = $selectedSubcommand -in 'new-tab', 'split-pane' # A top-level value option repeated after new-tab/split-pane still consumes its value, # so '1,1' in 'nt --pos 1,1' is not the start of the child commandline. if ($subcommandValueOptions[$selectedSubcommand] -ccontains $token -or ($isNewTerminal -and $topLevelValueOptions -ccontains $token)) { $expectingValueOption = $token } elseif ($isNewTerminal -and -not $token.StartsWith('-')) { $inCommandline = $true } } if ($inCommandline) { return } # Attached --opt=value: the option selects the value slot, the rest is the prefix. $attachedPrefix = '' $valueQuote = '' $slotValueOptions = if ($selectedSubcommand) { $subcommandValueOptions[$selectedSubcommand] } else { $topLevelValueOptions + $newTerminalValueOptions } if (-not $expectingValueOption -and $matchPrefix -match '^(--[^=]+)=(.*)$' -and $slotValueOptions -ccontains $Matches[1]) { $previousToken = $Matches[1] $attachedPrefix = $Matches[1] + '=' $attachedValue = Get-WtUnquotedWord -Text $Matches[2] $matchPrefix = $attachedValue.Word $valueQuote = $attachedValue.Quote } [object[]]$completionData = @() if (-not $selectedSubcommand) { if (($topLevelValueOptions + $newTerminalValueOptions) -ccontains $previousToken) { $completionData = Get-WtValueCompletionData -Option $previousToken } elseif ($matchPrefix -like '-*') { $completionData = $topLevelOptionData + $newTerminalOptionData } else { $completionData = $topLevelOptionData + $newTerminalOptionData + $subcommandData } } else { switch ($selectedSubcommand) { 'move-focus' { if ($matchPrefix -like '-*') { $completionData = @() } else { $completionData = $directionValueData } } 'swap-pane' { if ($matchPrefix -like '-*') { $completionData = @() } else { $completionData = $directionValueData } } default { if ($subcommandValueOptions[$selectedSubcommand] -ccontains $previousToken) { $completionData = Get-WtValueCompletionData -Option $previousToken } elseif ($matchPrefix -like '-*') { $completionData = $subcommandOptionData[$selectedSubcommand] } else { $completionData = @() } } } } # Whitespace or an argument-mode metacharacter would split or end a bare word. $quotePattern = '[\s{}();,|&<>''"`$]|^[@#]' foreach ($item in $completionData) { if ($item.Text -notlike "$matchPrefix*" -and $item.Display -notlike "$matchPrefix*") { continue } $completionText = [string]$item.Text if ($attachedPrefix -and -not $outerQuote) { if ($valueQuote -or $completionText -match $quotePattern) { $completionText = ConvertTo-WtQuotedValue -Value $completionText -Quote $valueQuote } $completionText = $attachedPrefix + $completionText } else { $completionText = $attachedPrefix + $completionText if ($outerQuote -or $completionText -match $quotePattern) { $completionText = ConvertTo-WtQuotedValue -Value $completionText -Quote $outerQuote } } [System.Management.Automation.CompletionResult]::new( $completionText, $item.Display, [System.Management.Automation.CompletionResultType]::$($item.Type), $item.Tooltip ) } } Register-ArgumentCompleter -Native -CommandName @('wt', 'wt.exe') -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) Complete-WtNative -wordToComplete $wordToComplete -commandAst $commandAst -cursorPosition $cursorPosition } |