Skip to content

Instantly share code, notes, and snippets.

@ninmonkey
Last active March 9, 2022 21:51
Show Gist options
  • Save ninmonkey/dcc8b0ec2e462ebf0d448fd0402592cd to your computer and use it in GitHub Desktop.
Save ninmonkey/dcc8b0ec2e462ebf0d448fd0402592cd to your computer and use it in GitHub Desktop.
Custom PowerShell 7 PSReadLikeKeyHandlers.ps1
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
# This example will replace any aliases on the command line with the resolved commands.
Set-PSReadLineKeyHandler -Key 'Alt+%' `
-BriefDescription ExpandAliases `
-LongDescription 'Replace all aliases with the full command' `
-ScriptBlock {
param($key, $arg)
$ast = $null
$tokens = $null
$errors = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$errors, [ref]$cursor)
$startAdjustment = 0
foreach ($token in $tokens) {
if ($token.TokenFlags -band [TokenFlags]::CommandName) {
$alias = $ExecutionContext.InvokeCommand.GetCommand($token.Extent.Text, 'Alias')
if ($alias -ne $null) {
$resolvedCommand = $alias.ResolvedCommandName
if ($resolvedCommand -ne $null) {
$extent = $token.Extent
$length = $extent.EndOffset - $extent.StartOffset
[Microsoft.PowerShell.PSConsoleReadLine]::Replace(
$extent.StartOffset + $startAdjustment,
$length,
$resolvedCommand)
# Our copy of the tokens won't have been updated, so we need to
# adjust by the difference in length
$startAdjustment += ($resolvedCommand.Length - $length)
}
}
}
}
}
<#
About:
Supports indenting/dedenting multiple lines of selected text
Like VS Code's hotkeys:
'ctrl+[' and 'ctrl+]'
Original from:
[Jaykul](https://gist.github.com/Jaykul/761c3b962e1f28998867e120c49c67ae)
#>
# Hack in a [ReadLine] accelerator
$xlr8r = [psobject].assembly.gettype('System.Management.Automation.TypeAccelerators')
if ($xlr8r::AddReplace) {
$xlr8r::AddReplace('ReadLine', [Microsoft.PowerShell.PSConsoleReadLine])
}
else {
$null = $xlr8r::Remove('ReadLine')
$xlr8r::Add('ReadLine', [Microsoft.PowerShell.PSConsoleReadLine])
}
# Set-PSReadLineKeyHandler -Key 'Alt+(' `
# -BriefDescription ParenthesizeSelection `
# -LongDescription 'Put parenthesis around the selection or entire line and move the cursor to after the closing parenthesis' `
# -ScriptBlock
# Now some key handlers
Set-PSReadLineKeyHandler 'Alt+]' `
-BriefDescription 'Indent/Dedent Selected text' `
-Description 'Indent/Dedent Selected text like VS Code ctrl+[ / ] ctrl+[' `
-ScriptBlock {
param($key, $arg)
$start = $null
$length = $null
[ReadLine]::GetSelectionState([ref]$start, [ref]$length)
$command = $null
$cursor = $null
[ReadLine]::GetBufferState([ref]$command, [ref]$cursor)
# if there's no selection, these should be set to the cursor
if ($start -lt 0) { $start = $cursor; $length = 0 }
$end = $start + $length
# Write-Host "`e[s`e[0;0H`e[32mStart:$start End:$end Length:$Length `e[u"
# pretend that entire lines are selected
if ($start -gt 0) {
$start = $command.SubString(0, $start).LastIndexOf("`n") + 1
}
$end = $end + $command.SubString($end).IndexOf("`n")
$length = $end - $start
# Write-Host "`e[s`e[2;0H`e[34mStart:$start End:$end Length:$Length `e[u"
$lines = $command.SubString($start, $length)
$count = ($lines -split "`n").Count
# Write-Host "`e[s`e[3;0H`e[36m$lines`e[u"
# Write-Host "`e[s`e[2;0H`e[34mStart:$start End:$end Length:$Length Lines:$Count`e[u"
[ReadLine]::Replace($start, $length, ($lines -replace '(?m)^', ' '))
[ReadLine]::SetCursorPosition($start)
[ReadLine]::SelectLine()
if ($count -gt 1) {
while (--$Count) {
[ReadLine]::SelectForwardChar()
[ReadLine]::SelectLine()
}
}
}
Set-PSReadLineKeyHandler 'Alt+[' `
-BriefDescription 'Indent/Dedent Selected text' `
-Description 'Indent/Dedent Selected text like VS Code ctrl+[/]' `
-ScriptBlock {
param($key, $arg)
$start = $null
$length = $null
[ReadLine]::GetSelectionState([ref]$start, [ref]$length)
$command = $null
$cursor = $null
[ReadLine]::GetBufferState([ref]$command, [ref]$cursor)
# if there's no selection, these should be set to the cursor
if ($start -lt 0) { $start = $cursor; $length = 0 }
$end = $start + $length
# Write-Host "`e[s`e[0;0H`e[32mStart:$start End:$end Length:$Length `e[u"
# pretend that entire lines are selected
if ($start -gt 0) {
$start = $command.SubString(0, $start).LastIndexOf("`n") + 1
}
$end = $end + $command.SubString($end).IndexOf("`n")
$length = $end - $start
# Write-Host "`e[s`e[2;0H`e[34mStart:$start End:$end Length:$Length `e[u"
$lines = $command.SubString($start, $length)
$count = ($lines -split "`n").Count
# Write-Host "`e[s`e[3;0H`e[36m$lines`e[u"
# Write-Host "`e[s`e[2;0H`e[34mStart:$start End:$end Length:$Length Lines:$Count`e[u"
[ReadLine]::Replace($start, $length, ($lines -replace '(?m)^ ', ''))
[ReadLine]::SetCursorPosition($start)
[ReadLine]::SelectLine()
if ($count -gt 1) {
while (--$Count) {
[ReadLine]::SelectForwardChar()
[ReadLine]::SelectLine()
}
}
}
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
# Sometimes you want to get a property of invoke a member on what you've entered so far
# but you need parens to do that. This binding will help by putting parens around the current selection,
# or if nothing is selected, the whole line.
Set-PSReadLineKeyHandler -Key 'Alt+(' `
-BriefDescription ParenthesizeSelection `
-LongDescription 'Put parenthesis around the selection or entire line and move the cursor to after the closing parenthesis' `
-ScriptBlock {
param($key, $arg)
$selectionStart = $null
$selectionLength = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
if ($selectionStart -ne -1) {
[Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, '(' + $line.SubString($selectionStart, $selectionLength) + ')')
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
}
else {
[Microsoft.PowerShell.PSConsoleReadLine]::Replace(0, $line.Length, '(' + $line + ')')
[Microsoft.PowerShell.PSConsoleReadLine]::EndOfLine()
}
}
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
#region Smart Insert/Delete
<# Source: official PSReadlineModule sample
about:
Auto opens/closes/jumps over: {}()[]
#>
# The next four key handlers are designed to make entering matched quotes
# parens, and braces a nicer experience. I'd like to include functions
# in the module that do this, but this implementation still isn't as smart
# as ReSharper, so I'm just providing it as a sample.
Set-PSReadLineKeyHandler -Key '"', "'" `
-BriefDescription SmartInsertQuote `
-LongDescription 'Warning: Can act strange if combined with "Toggle Qoute Argument". Insert paired quotes if not already on a quote' `
-ScriptBlock {
param($key, $arg)
$quote = $key.KeyChar
$selectionStart = $null
$selectionLength = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
# If text is selected, just quote it without any smarts
if ($selectionStart -ne -1) {
[Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, $quote + $line.SubString($selectionStart, $selectionLength) + $quote)
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
return
}
$ast = $null
$tokens = $null
$parseErrors = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$parseErrors, [ref]$null)
function FindToken {
param($tokens, $cursor)
foreach ($token in $tokens) {
if ($cursor -lt $token.Extent.StartOffset) { continue }
if ($cursor -lt $token.Extent.EndOffset) {
$result = $token
$token = $token -as [StringExpandableToken]
if ($token) {
$nested = FindToken $token.NestedTokens $cursor
if ($nested) { $result = $nested }
}
return $result
}
}
return $null
}
$token = FindToken $tokens $cursor
# If we're on or inside a **quoted** string token (so not generic), we need to be smarter
if ($token -is [StringToken] -and $token.Kind -ne [TokenKind]::Generic) {
# If we're at the start of the string, assume we're inserting a new string
if ($token.Extent.StartOffset -eq $cursor) {
[Microsoft.PowerShell.PSConsoleReadLine]::Insert("$quote$quote ")
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
return
}
# If we're at the end of the string, move over the closing quote if present.
if ($token.Extent.EndOffset -eq ($cursor + 1) -and $line[$cursor] -eq $quote) {
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
return
}
}
if ($null -eq $token -or
$token.Kind -eq [TokenKind]::RParen -or $token.Kind -eq [TokenKind]::RCurly -or $token.Kind -eq [TokenKind]::RBracket) {
if ($line[0..$cursor].Where{ $_ -eq $quote }.Count % 2 -eq 1) {
# Odd number of quotes before the cursor, insert a single quote
[Microsoft.PowerShell.PSConsoleReadLine]::Insert($quote)
}
else {
# Insert matching quotes, move cursor to be in between the quotes
[Microsoft.PowerShell.PSConsoleReadLine]::Insert("$quote$quote")
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
}
return
}
# If cursor is at the start of a token, enclose it in quotes.
if ($token.Extent.StartOffset -eq $cursor) {
if ($token.Kind -eq [TokenKind]::Generic -or $token.Kind -eq [TokenKind]::Identifier -or
$token.Kind -eq [TokenKind]::Variable -or $token.TokenFlags.hasFlag([TokenFlags]::Keyword)) {
$end = $token.Extent.EndOffset
$len = $end - $cursor
[Microsoft.PowerShell.PSConsoleReadLine]::Replace($cursor, $len, $quote + $line.SubString($cursor, $len) + $quote)
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($end + 2)
return
}
}
# We failed to be smart, so just insert a single quote
[Microsoft.PowerShell.PSConsoleReadLine]::Insert($quote)
}
Set-PSReadLineKeyHandler -Key '(', '{', '[' `
-BriefDescription InsertPairedBraces `
-LongDescription 'Insert matching braces' `
-ScriptBlock {
param($key, $arg)
$closeChar = switch ($key.KeyChar) {
<#case#> '(' { [char]')'; break }
<#case#> '{' { [char]'}'; break }
<#case#> '[' { [char]']'; break }
}
$selectionStart = $null
$selectionLength = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
if ($selectionStart -ne -1) {
# Text is selected, wrap it in brackets
[Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, $key.KeyChar + $line.SubString($selectionStart, $selectionLength) + $closeChar)
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
}
else {
# No text is selected, insert a pair
[Microsoft.PowerShell.PSConsoleReadLine]::Insert("$($key.KeyChar)$closeChar")
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
}
}
Set-PSReadLineKeyHandler -Key ')', ']', '}' `
-BriefDescription SmartCloseBraces `
-LongDescription 'Insert closing brace or skip' `
-ScriptBlock {
param($key, $arg)
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
if ($line[$cursor] -eq $key.KeyChar) {
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
}
else {
[Microsoft.PowerShell.PSConsoleReadLine]::Insert("$($key.KeyChar)")
}
}
Set-PSReadLineKeyHandler -Key Backspace `
-BriefDescription SmartBackspace `
-LongDescription 'Delete previous character or matching quotes/parens/braces' `
-ScriptBlock {
param($key, $arg)
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
if ($cursor -gt 0) {
$toMatch = $null
if ($cursor -lt $line.Length) {
switch ($line[$cursor]) {
<#case#> '"' { $toMatch = '"'; break }
<#case#> "'" { $toMatch = "'"; break }
<#case#> ')' { $toMatch = '('; break }
<#case#> ']' { $toMatch = '['; break }
<#case#> '}' { $toMatch = '{'; break }
}
}
if ($toMatch -ne $null -and $line[$cursor - 1] -eq $toMatch) {
[Microsoft.PowerShell.PSConsoleReadLine]::Delete($cursor - 1, 2)
}
else {
[Microsoft.PowerShell.PSConsoleReadLine]::BackwardDeleteChar($key, $arg)
}
}
}
#endregion Smart Insert/Delete
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
# Each time you press Alt+', this key handler will change the token
# under or before the cursor. It will cycle through single quotes, double quotes, or
# no quotes each time it is invoked.
Set-PSReadLineKeyHandler -Key "Alt+'" `
-BriefDescription ToggleQuoteArgument `
-LongDescription 'Warning: Can act strange if combined with 'Smart Brackets Braces Parens.ps1'. Toggle quotes on the argument under the cursor' `
-ScriptBlock {
param($key, $arg)
$ast = $null
$tokens = $null
$errors = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$errors, [ref]$cursor)
$tokenToChange = $null
foreach ($token in $tokens) {
$extent = $token.Extent
if ($extent.StartOffset -le $cursor -and $extent.EndOffset -ge $cursor) {
$tokenToChange = $token
# If the cursor is at the end (it's really 1 past the end) of the previous token,
# we only want to change the previous token if there is no token under the cursor
if ($extent.EndOffset -eq $cursor -and $foreach.MoveNext()) {
$nextToken = $foreach.Current
if ($nextToken.Extent.StartOffset -eq $cursor) {
$tokenToChange = $nextToken
}
}
break
}
}
if ($tokenToChange -ne $null) {
$extent = $tokenToChange.Extent
$tokenText = $extent.Text
if ($tokenText[0] -eq '"' -and $tokenText[-1] -eq '"') {
# Switch to no quotes
$replacement = $tokenText.Substring(1, $tokenText.Length - 2)
}
elseif ($tokenText[0] -eq "'" -and $tokenText[-1] -eq "'") {
# Switch to double quotes
$replacement = '"' + $tokenText.Substring(1, $tokenText.Length - 2) + '"'
}
else {
# Add single quotes
$replacement = "'" + $tokenText + "'"
}
[Microsoft.PowerShell.PSConsoleReadLine]::Replace(
$extent.StartOffset,
$tokenText.Length,
$replacement)
}
}
@ninmonkey
Copy link
Author

credits to Jaykul and the PSReadLine repo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment