2026-07-17 22:23:00 -05:00
using namespace System . Management . Automation
using namespace System . Management . Automation . Language
if ( Get-Command Set-PSReadLineOption -ErrorAction SilentlyContinue ) {
2026-07-17 23:05:43 -05:00
# Set EditMode Windows first to ensure all subsequent registrations target the correct keymap
Set-PSReadLineOption -EditMode Windows
2026-07-17 22:23:00 -05:00
# Searching for commands with up/down arrow is really handy.
Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward
Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward
# This key handler shows the entire or filtered history using Out-GridView.
Set-PSReadLineKeyHandler -Key F7 `
-BriefDescription History `
-LongDescription 'Show command history' `
-ScriptBlock {
$pattern = $null
[ Microsoft.PowerShell.PSConsoleReadLine ]:: GetBufferState ([ ref ] $pattern , [ ref ] $null )
if ( $pattern ) {
$pattern = [ regex ]:: Escape ( $pattern )
}
$history = [ System.Collections.ArrayList ] @ (
$last = ''
$lines = ''
foreach ( $line in [ System.IO.File ]:: ReadLines (( Get-PSReadLineOption ). HistorySavePath )) {
if ( $line . EndsWith ( '`' )) {
$line = $line . Substring ( 0 , $line . Length - 1 )
$lines = if ( $lines ) {
" $lines `n $line "
}
else {
$line
}
continue
}
if ( $lines ) {
$line = " $lines `n $line "
$lines = ''
}
if (( $line -cne $last ) -and (! $pattern -or ( $line -match $pattern ))) {
$last = $line
$line
}
}
)
$history . Reverse ()
$command = $history | Out-GridView -Title History -PassThru
if ( $command ) {
[ Microsoft.PowerShell.PSConsoleReadLine ]:: RevertLine ()
[ Microsoft.PowerShell.PSConsoleReadLine ]:: Insert (( $command -join " `n " ))
}
}
# CaptureScreen is good for blog posts or email showing a transaction
Set-PSReadLineKeyHandler -Chord 'Ctrl+d,Ctrl+c' -Function CaptureScreen
# The built-in word movement uses character delimiters, but token based word movement is also very useful
Set-PSReadLineKeyHandler -Key Alt + d -Function ShellKillWord
Set-PSReadLineKeyHandler -Key Alt + Backspace -Function ShellBackwardKillWord
Set-PSReadLineKeyHandler -Key Alt + b -Function ShellBackwardWord
Set-PSReadLineKeyHandler -Key Alt + f -Function ShellForwardWord
Set-PSReadLineKeyHandler -Key Alt + B -Function SelectShellBackwardWord
Set-PSReadLineKeyHandler -Key Alt + F -Function SelectShellForwardWord
#region Smart Insert/Delete
Set-PSReadLineKeyHandler -Key '"' , "'" `
-BriefDescription SmartInsertQuote `
-LongDescription "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
# Sometimes you enter a command but realize you forgot to do something else first.
Set-PSReadLineKeyHandler -Key Alt + w `
-BriefDescription SaveInHistory `
-LongDescription "Save current line in history but do not execute" `
-ScriptBlock {
param ( $key , $arg )
$line = $null
$cursor = $null
[ Microsoft.PowerShell.PSConsoleReadLine ]:: GetBufferState ([ ref ] $line , [ ref ] $cursor )
[ Microsoft.PowerShell.PSConsoleReadLine ]:: AddToHistory ( $line )
[ Microsoft.PowerShell.PSConsoleReadLine ]:: RevertLine ()
}
# Insert text from the clipboard as a here string
Set-PSReadLineKeyHandler -Key Ctrl + V `
-BriefDescription PasteAsHereString `
-LongDescription "Paste the clipboard text as a here string" `
-ScriptBlock {
param ( $key , $arg )
Add-Type -Assembly PresentationCore
if ([ System.Windows.Clipboard ]:: ContainsText ()) {
# Get clipboard text - remove trailing spaces, convert \r\n to \n, and remove the final \n.
$text = ([ System.Windows.Clipboard ]:: GetText () -replace "\p{Zs}* `r ? `n " , " `n " ). TrimEnd ()
[ Microsoft.PowerShell.PSConsoleReadLine ]:: Insert ( "@' `n $text `n '@" )
}
else {
[ Microsoft.PowerShell.PSConsoleReadLine ]:: Ding ()
}
}
# Sometimes you want to get a property of invoke a member on what you've entered so far
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 ()
}
}
# Cycle through quote styles on current token
Set-PSReadLineKeyHandler -Key "Alt+'" `
-BriefDescription ToggleQuoteArgument `
-LongDescription "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 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 )
}
}
# Replace aliases on command line with full 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 )
# Adjust by the difference in length
$startAdjustment += ( $resolvedCommand . Length - $length )
}
}
}
}
}
# F1 for help on the command line
Set-PSReadLineKeyHandler -Key F1 `
-BriefDescription CommandHelp `
-LongDescription "Open the help window for the current 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 )
$commandAst = $ast . FindAll ( {
$node = $args [ 0 ]
$node -is [ CommandAst ] -and
$node . Extent . StartOffset -le $cursor -and
$node . Extent . EndOffset -ge $cursor
}, $true ) | Select-Object -Last 1
if ( $commandAst -ne $null ) {
$commandName = $commandAst . GetCommandName ()
if ( $commandName -ne $null ) {
$command = $ExecutionContext . InvokeCommand . GetCommand ( $commandName , 'All' )
if ( $command -is [ AliasInfo ]) {
$commandName = $command . ResolvedCommandName
}
if ( $commandName -ne $null ) {
Get-Help $commandName -ShowWindow
}
}
}
}
# Directory Marks Jumping system
$global:PSReadLineMarks = @ {}
Set-PSReadLineKeyHandler -Key Ctrl + J `
-BriefDescription MarkDirectory `
-LongDescription "Mark the current directory" `
-ScriptBlock {
param ( $key , $arg )
$key = [ Console ]:: ReadKey ( $true )
$global:PSReadLineMarks [ $key . KeyChar ] = $pwd
}
Set-PSReadLineKeyHandler -Key Ctrl + j `
-BriefDescription JumpDirectory `
-LongDescription "Goto the marked directory" `
-ScriptBlock {
param ( $key , $arg )
$key = [ Console ]:: ReadKey ()
$dir = $global:PSReadLineMarks [ $key . KeyChar ]
if ( $dir ) {
cd $dir
[ Microsoft.PowerShell.PSConsoleReadLine ]:: InvokePrompt ()
}
}
Set-PSReadLineKeyHandler -Key Alt + j `
-BriefDescription ShowDirectoryMarks `
-LongDescription "Show the currently marked directories" `
-ScriptBlock {
param ( $key , $arg )
$global:PSReadLineMarks . GetEnumerator () | % {
[ PSCustomObject ] @ { Key = $_ . Key ; Dir = $_ . Value } } |
Format-Table -AutoSize | Out-Host
[ Microsoft.PowerShell.PSConsoleReadLine ]:: InvokePrompt ()
}
# Make RightArrow accept the next word in prediction suggestion instead of whole line
Set-PSReadLineKeyHandler -Key RightArrow `
-BriefDescription ForwardCharAndAcceptNextSuggestionWord `
-LongDescription "Move cursor one character to the right in the current editing line and accept the next word in suggestion when it's at the end of current editing line" `
-ScriptBlock {
param ( $key , $arg )
$line = $null
$cursor = $null
[ Microsoft.PowerShell.PSConsoleReadLine ]:: GetBufferState ([ ref ] $line , [ ref ] $cursor )
if ( $cursor -lt $line . Length ) {
[ Microsoft.PowerShell.PSConsoleReadLine ]:: ForwardChar ( $key , $arg )
}
else {
[ Microsoft.PowerShell.PSConsoleReadLine ]:: AcceptNextSuggestionWord ( $key , $arg )
}
}
# Cycle/Select Command Arguments
Set-PSReadLineKeyHandler -Key Alt + a `
-BriefDescription SelectCommandArguments `
-LongDescription "Set current selection to next command argument in the command line. Use of digit argument selects argument by position" `
-ScriptBlock {
param ( $key , $arg )
$ast = $null
$cursor = $null
[ Microsoft.PowerShell.PSConsoleReadLine ]:: GetBufferState ([ ref ] $ast , [ ref ] $null , [ ref ] $null , [ ref ] $cursor )
$asts = $ast . FindAll ( {
$args [ 0 ] -is [ ExpressionAst ] -and
$args [ 0 ]. Parent -is [ CommandAst ] -and
$args [ 0 ]. Extent . StartOffset -ne $args [ 0 ]. Parent . Extent . StartOffset
}, $true )
if ( $asts . Count -eq 0 ) {
[ Microsoft.PowerShell.PSConsoleReadLine ]:: Ding ()
return
}
$nextAst = $null
if ( $null -ne $arg ) {
$nextAst = $asts [ $arg - 1 ]
}
else {
foreach ( $ast in $asts ) {
if ( $ast . Extent . StartOffset -ge $cursor ) {
$nextAst = $ast
break
}
}
if ( $null -eq $nextAst ) {
$nextAst = $asts [ 0 ]
}
}
$startOffsetAdjustment = 0
$endOffsetAdjustment = 0
if ( $nextAst -is [ StringConstantExpressionAst ] -and
$nextAst . StringConstantType -ne [ StringConstantType ]:: BareWord ) {
$startOffsetAdjustment = 1
$endOffsetAdjustment = 2
}
[ Microsoft.PowerShell.PSConsoleReadLine ]:: SetCursorPosition ( $nextAst . Extent . StartOffset + $startOffsetAdjustment )
[ Microsoft.PowerShell.PSConsoleReadLine ]:: SetMark ( $null , $null )
[ Microsoft.PowerShell.PSConsoleReadLine ]:: SelectForwardChar ( $null , ( $nextAst . Extent . EndOffset - $nextAst . Extent . StartOffset ) - $endOffsetAdjustment )
}
Set-PSReadLineOption -PredictionSource History
Set-PSReadLineOption -PredictionViewStyle ListView
# dotnet build macro
Set-PSReadLineKeyHandler -Key Ctrl + Shift + b `
-BriefDescription BuildCurrentDirectory `
-LongDescription "Build the current directory" `
-ScriptBlock {
[ Microsoft.PowerShell.PSConsoleReadLine ]:: RevertLine ()
[ Microsoft.PowerShell.PSConsoleReadLine ]:: Insert ( "dotnet build" )
[ Microsoft.PowerShell.PSConsoleReadLine ]:: AcceptLine ()
}
# dotnet test macro
Set-PSReadLineKeyHandler -Key Ctrl + Shift + t `
-BriefDescription BuildCurrentDirectory `
-LongDescription "Build the current directory" `
-ScriptBlock {
[ Microsoft.PowerShell.PSConsoleReadLine ]:: RevertLine ()
[ Microsoft.PowerShell.PSConsoleReadLine ]:: Insert ( "dotnet test" )
[ Microsoft.PowerShell.PSConsoleReadLine ]:: AcceptLine ()
}
Set-PSReadLineOption -Colors @ {
"Parameter" = [ ConsoleColor ]:: DarkBlue
}
}
function DisableReadline {
if ( Get-Command Set-PSReadLineOption -ErrorAction SilentlyContinue ) {
Set-PSReadLineOption -PredictionSource None
}
}
function EnableReadline {
if ( Get-Command Set-PSReadLineOption -ErrorAction SilentlyContinue ) {
Set-PSReadLineOption -PredictionSource History
}
}