DirJump.psm1
|
# DirJump: PowerShell side (prompt hook + key handlers). The predictor itself lives in DirJump.dll. # # Keys (each bound only if not already bound to something else): # Ctrl+j jump to the top DirJump match for what is typed (no history entry) # Backspace on an empty line cd .. (guarded: never fires as part of deleting text / key repeat) # Enter, Tab, arrows etc. are untouched: Enter always runs what was typed. Set-StrictMode -Version Latest $script:BackspaceGuardMs = 500 $script:LastDeleteTicks = [long]0 $script:BoundKeys = @() function script:Get-FileSystemLocation { $loc = $ExecutionContext.SessionState.Path.CurrentLocation if ($loc.Provider.Name -eq 'FileSystem') { $loc.ProviderPath } else { $null } } # --- prompt hook ------------------------------------------------------------------------------- $script:OriginalPrompt = $null $promptCmd = Get-Command -Name prompt -CommandType Function -ErrorAction SilentlyContinue if ($promptCmd) { $script:OriginalPrompt = $promptCmd.ScriptBlock } $script:CallerState = $null $script:LastHistoryId = -1 # Moves made by the key handlers must go through the *global* session state: PowerShell keeps a # separate `cd -`/`cd +` history per session state, and module code runs in the module's own. function script:Set-DirJumpLocation([string]$Path) { if ($script:CallerState) { $sb = [scriptblock]::Create('Set-Location -LiteralPath $args[0]') [void]$ExecutionContext.InvokeCommand.InvokeScript($script:CallerState, $sb, $Path) } else { Set-Location -LiteralPath $Path } } # Functions and handlers defined here stay bound to the module scope, so $script: works inside them. function global:prompt { [CmdletBinding()] param() # The host calls prompt from the global scope, so $PSCmdlet.SessionState is the global session # state there. (When PSReadLine re-invokes it from a key handler, the caller is this module: ignore.) if ($null -eq $PSCmdlet.SessionState.Module) { $script:CallerState = $PSCmdlet.SessionState } $p = Get-FileSystemLocation if ($p) { # The command line that just ran, if any: lets DirJump recognise a suggestion picked from the list. $last = $null $h = Get-History -Count 1 if ($h -and $h.Id -ne $script:LastHistoryId) { $script:LastHistoryId = $h.Id; $last = $h.CommandLine } [DirJump.State]::OnPrompt($p, $last) } $text = if ($script:OriginalPrompt) { & $script:OriginalPrompt } else { "PS $($ExecutionContext.SessionState.Path.CurrentLocation)$('>' * ($NestedPromptLevel + 1)) " } # OSC 9;9 lets Windows Terminal open new tabs/panes (and restored sessions) in this directory. if ($p) { "$([char]27)]9;9;`"$p`"$([char]27)\" + $text } else { $text } } # --- key handlers ------------------------------------------------------------------------------ function script:Test-KeyFree([string]$Chord) { -not (Get-PSReadLineKeyHandler -Bound | Where-Object { $_.Key -eq $Chord }) } # PSReadLine is only loaded in interactive sessions; elsewhere (scripts, tests) skip the keys. $script:HasPSReadLine = [bool](Get-Module PSReadLine) if (-not $script:HasPSReadLine) { # no key handlers } elseif (Test-KeyFree 'Ctrl+j') { Set-PSReadLineKeyHandler -Chord 'Ctrl+j' -BriefDescription 'DirJump' ` -Description 'Jump to the best DirJump directory match for the current input' -ScriptBlock { $line = $null; $cursor = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) $target = [DirJump.State]::Jump($line) # also tags the move as a jump for this query if (-not $target) { return } # Clear the typed query. (RevertLine is not used: with the ListView open it only dismisses the list.) [Microsoft.PowerShell.PSConsoleReadLine]::Delete(0, $line.Length) Set-DirJumpLocation $target [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() } $script:BoundKeys += 'Ctrl+j' } else { Write-Warning 'DirJump: Ctrl+j is already bound; not binding the jump key.' } $bs = if ($script:HasPSReadLine) { Get-PSReadLineKeyHandler -Bound | Where-Object { $_.Key -eq 'Backspace' } } if (-not $script:HasPSReadLine) { # no key handlers } elseif (-not $bs -or $bs.Function -eq 'BackwardDeleteChar') { Set-PSReadLineKeyHandler -Key Backspace -BriefDescription 'DirJumpBackspace' ` -Description 'Delete the previous character; on an empty line, go to the parent directory' -ScriptBlock { param($key, $arg) $line = $null; $cursor = $null [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor) $now = [Environment]::TickCount64 if ($line.Length -gt 0) { [Microsoft.PowerShell.PSConsoleReadLine]::BackwardDeleteChar($key, $arg) $script:LastDeleteTicks = $now return } # Guard: a Backspace arriving soon after one that deleted text (e.g. key repeat while # clearing the line) is swallowed, and extends the guard while the key is held. if ($now - $script:LastDeleteTicks -lt $script:BackspaceGuardMs) { $script:LastDeleteTicks = $now return } $loc = $ExecutionContext.SessionState.Path.CurrentLocation if ($loc.Provider.Name -ne 'FileSystem') { return } # (Split-Path can't combine -LiteralPath with -Parent.) Null at a drive root. $parent = [System.IO.Path]::GetDirectoryName($loc.ProviderPath) if (-not $parent) { return } [DirJump.State]::SetPending('up', $null) Set-DirJumpLocation $parent [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() } $script:BoundKeys += 'Backspace' } else { Write-Warning "DirJump: Backspace is bound to $($bs.Function); not adding cd-on-empty-line." } # --- commands ---------------------------------------------------------------------------------- function Get-DirJumpStatus { <# .SYNOPSIS Show DirJump's indexes, history file and ignore configuration. #> [DirJump.State]::Status() } function Get-DirJumpMatch { <# .SYNOPSIS Show how DirJump ranks matches for a query, with the score breakdown. #> param([Parameter(Mandatory, Position = 0)][string]$Query, [int]$First = 10) [DirJump.State]::Query($Query, $First) | Select-Object Display, Score, Explanation, FullPath } function Update-DirJumpIndex { <# .SYNOPSIS Rebuild the directory indexes now (they also refresh automatically). #> param([switch]$Wait) [DirJump.State]::Reindex() $p = Get-FileSystemLocation if ($p) { [DirJump.State]::OnPrompt($p) } if ($Wait) { [void][DirJump.State]::WaitForIndex(60000) } } # --- init / cleanup ---------------------------------------------------------------------------- $start = Get-FileSystemLocation if (-not $start) { $start = $HOME } [DirJump.State]::Initialize($start) $ExecutionContext.SessionState.Module.OnRemove = { foreach ($k in $script:BoundKeys) { Remove-PSReadLineKeyHandler -Chord $k } if ($script:BoundKeys -contains 'Backspace') { Set-PSReadLineKeyHandler -Key Backspace -Function BackwardDeleteChar } if ($script:OriginalPrompt) { Set-Item -Path function:global:prompt -Value $script:OriginalPrompt } } Export-ModuleMember -Function Get-DirJumpStatus, Get-DirJumpMatch, Update-DirJumpIndex |