PSFzf.psm1
# AUTOGENERATED - DO NOT EDIT # PSFzf.Base.ps1 param( [parameter(Position=0,Mandatory=$false)][string]$PSReadlineChordProvider = 'Ctrl+t', [parameter(Position=1,Mandatory=$false)][string]$PSReadlineChordReverseHistory = 'Ctrl+r', [parameter(Position=2,Mandatory=$false)][string]$PSReadlineChordSetLocation = 'Alt+c', [parameter(Position=3,Mandatory=$false)][string]$PSReadlineChordReverseHistoryArgs = 'Alt+a') $script:IsWindows = ($PSVersionTable.PSVersion.Major -le 5) -or $IsWindows if ($script:IsWindows) { $script:ShellCmd = 'cmd.exe /S /C {0}' $script:DefaultFileSystemCmd = @" dir /s/b "{0}" "@ $script:DefaultFileSystemCmdDirOnly = @" dir /s/b/ad "{0}" "@ } else { $script:ShellCmd = '/bin/sh -c "{0}"' $script:DefaultFileSystemCmd = @" find -L '{0}' -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null "@ $script:DefaultFileSystemCmdDirOnly = @" find -L '{0}' -path '*/\.*' -prune -o -type d -print 2> /dev/null "@ } $script:RunningInWindowsTerminal = [bool]($env:WT_Session) -or [bool]($env:ConEmuANSI) if ($script:RunningInWindowsTerminal) { $script:DefaultFileSystemFdCmd = "fd.exe --color always . --full-path `"{0}`" --fixed-strings" } else { $script:DefaultFileSystemFdCmd = "fd.exe . --full-path `"{0}`" --fixed-strings" } $script:UseFd = $false $script:AltCCommand = [ScriptBlock]{ param($Location) Set-Location $Location } function Get-FileSystemCmd { param($dir, [switch]$dirOnly = $false) # Note that there is no way to know how to list only directories using # FZF_DEFAULT_COMMAND, so we never use it in that case. if ($dirOnly -or [string]::IsNullOrWhiteSpace($env:FZF_DEFAULT_COMMAND)) { if ($script:UseFd) { if ($dirOnly) { "$($script:DefaultFileSystemFdCmd -f $dir) --type directory" } else { $script:DefaultFileSystemFdCmd -f $dir } } else { $cmd = $script:DefaultFileSystemCmd if ($dirOnly) { $cmd = $script:DefaultFileSystemCmdDirOnly } $script:ShellCmd -f ($cmd -f $dir) } } else { $script:ShellCmd -f ($env:FZF_DEFAULT_COMMAND -f $dir) } } class FzfDefaultOpts { [bool]$UsePsFzfOpts [string]$PrevEnv [bool]$Restored FzfDefaultOpts([string]$tempVal) { $this.UsePsFzfOpts = -not [string]::IsNullOrWhiteSpace($env:_PSFZF_FZF_DEFAULT_OPTS) $this.PrevEnv = $env:FZF_DEFAULT_OPTS $env:FZF_DEFAULT_OPTS = $this.Get() + " " + $tempVal } [string]Get() { if ($this.UsePsFzfOpts) { return $env:_PSFZF_FZF_DEFAULT_OPTS; } else { return $env:FZF_DEFAULT_OPTS; } } [void]Restore() { $env:FZF_DEFAULT_OPTS = $this.PrevEnv } } class FzfDefaultCmd { [string]$PrevEnv FzfDefaultCmd([string]$overrideVal) { $this.PrevEnv = $env:FZF_DEFAULT_COMMAND $env:FZF_DEFAULT_COMMAND = $overrideVal } [void]Restore() { $env:FZF_DEFAULT_COMMAND = $this.PrevEnv } } function FixCompletionResult($str) { if ([string]::IsNullOrEmpty($str)) { return "" } elseif ($str.Contains(" ") -or $str.Contains("`t")) { $str = $str.Replace("`r`n","") # check if already quoted if (($str.StartsWith("'") -and $str.EndsWith("'")) -or ` ($str.StartsWith("""") -and $str.EndsWith(""""))) { return $str } else { return """{0}""" -f $str } } else { return $str.Replace("`r`n","") } } #HACK: workaround for fact that PSReadLine seems to clear screen # after keyboard shortcut action is executed, and to work around a UTF8 # PSReadLine issue (GitHub PSFZF issue #71) function InvokePromptHack() { $previousOutputEncoding = [Console]::OutputEncoding [Console]::OutputEncoding = [Text.Encoding]::UTF8 try { [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() } finally { [Console]::OutputEncoding = $previousOutputEncoding } } $script:FzfLocation = $null $script:OverrideFzfDefaults = $null $script:PSReadlineHandlerChords = @() $script:TabContinuousTrigger = [IO.Path]::DirectorySeparatorChar.ToString() $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { $PsReadlineShortcuts.Values | Where-Object Chord | ForEach-Object { Remove-PSReadlineKeyHandler $_.Chord } RemovePsFzfAliases RemoveGitKeyBindings } # if the quoted string ends with a '\', and we need to escape it for Windows: function script:PrepareArg($argStr) { if (-not $argStr.EndsWith("\\") -and $argStr.EndsWith('\')) { return $argStr + '\' } else { return $argStr } } function Set-PsFzfOption{ param( [switch] $TabExpansion, [string] $PSReadlineChordProvider, [string] $PSReadlineChordReverseHistory, [string] $PSReadlineChordSetLocation, [string] $PSReadlineChordReverseHistoryArgs, [switch] $GitKeyBindings, [switch] $EnableAliasFuzzyEdit, [switch] $EnableAliasFuzzyFasd, [switch] $EnableAliasFuzzyHistory, [switch] $EnableAliasFuzzyKillProcess, [switch] $EnableAliasFuzzySetLocation, [switch] $EnableAliasFuzzyScoop, [switch] $EnableAliasFuzzySetEverything, [switch] $EnableAliasFuzzyZLocation, [switch] $EnableAliasFuzzyGitStatus, [switch] $EnableFd, [ScriptBlock] $AltCCommand ) if ($PSBoundParameters.ContainsKey('TabExpansion')) { SetTabExpansion $TabExpansion } if ($PSBoundParameters.ContainsKey('GitKeyBindings')) { SetGitKeyBindings $GitKeyBindings } $PsReadlineShortcuts.GetEnumerator() | ForEach-Object { if ($PSBoundParameters.ContainsKey($_.key)) { $info = $_.value $newChord = $PSBoundParameters[$_.key] $result = SetPsReadlineShortcut $newChord -Override $info.BriefDesc $info.Desc $info.ScriptBlock if ($result) { if (($null -ne $info.Chord) -and ($info.Chord.ToLower() -ne $newChord.ToLower())) { Remove-PSReadLineKeyHandler $info.Chord } $info.Chord = $newChord } } } if ($EnableAliasFuzzyEdit) { SetPsFzfAlias "fe" Invoke-FuzzyEdit} if ($EnableAliasFuzzyFasd) { SetPsFzfAlias "ff" Invoke-FuzzyFasd} if ($EnableAliasFuzzyHistory) { SetPsFzfAlias "fh" Invoke-FuzzyHistory } if ($EnableAliasFuzzyKillProcess) { SetPsFzfAlias "fkill" Invoke-FuzzyKillProcess } if ($EnableAliasFuzzySetLocation) { SetPsFzfAlias "fd" Invoke-FuzzySetLocation } if ($EnableAliasFuzzyZLocation) { SetPsFzfAlias "fz" Invoke-FuzzyZLocation } if ($EnableAliasFuzzyGitStatus) { SetPsFzfAlias "fgs" Invoke-FuzzyGitStatus } if ($EnableAliasFuzzyScoop) { SetPsFzfAlias "fs" Invoke-FuzzyScoop } if ($EnableAliasFuzzySetEverything) { if (${function:Set-LocationFuzzyEverything}) { SetPsFzfAlias "cde" Set-LocationFuzzyEverything } } if ($PSBoundParameters.ContainsKey('EnableFd')) { $script:UseFd = $EnableFd } if ($PSBoundParameters.ContainsKey('TabContinuousTrigger')) { $script:TabContinuousTrigger = $TabContinuousTrigger } if ($PSBoundParameters.ContainsKey('AltCCommand')) { $script:AltCCommand = $AltCCommand } } function Stop-Pipeline { # borrowed from https://stackoverflow.com/a/34800670: (Add-Type -Passthru -TypeDefinition ' using System.Management.Automation; namespace PSFzf.IO { public static class CustomPipelineStopper { public static void Stop(Cmdlet cmdlet) { throw (System.Exception) System.Activator.CreateInstance(typeof(Cmdlet).Assembly.GetType("System.Management.Automation.StopUpstreamCommandsException"), cmdlet); } } }')::Stop($PSCmdlet) } function Invoke-Fzf { param( # Search [Alias("x")] [switch]$Extended, [Alias('e')] [switch]$Exact, [Alias('i')] [switch]$CaseInsensitive, [switch]$CaseSensitive, [Alias('d')] [string]$Delimiter, [switch]$NoSort, [Alias('tac')] [switch]$ReverseInput, [switch]$Phony, [ValidateSet('length','begin','end','index')] [string] $Tiebreak = $null, # Interface [Alias('m')] [switch]$Multi, [switch]$NoMouse, [string[]]$Bind, [switch]$Cycle, [switch]$KeepRight, [switch]$NoHScroll, [switch]$FilepathWord, # Layout [ValidatePattern("^[1-9]+[0-9]+$|^[1-9][0-9]?%?$|^100%?$")] [string]$Height, [ValidateRange(1,[int]::MaxValue)] [int]$MinHeight, [ValidateSet('default','reverse','reverse-list')] [string]$Layout = $null, [switch]$Border, [ValidateSet('rounded','sharp','horizontal')] [string]$BorderStyle, [ValidateSet('default','inline','hidden')] [string]$Info = $null, [string]$Prompt, [string]$Pointer, [string]$Marker, [string]$Header, [int]$HeaderLines = -1, # Display [switch]$Ansi, [int]$Tabstop = 8, [string]$Color, [switch]$NoBold, # History [string]$History, [int]$HistorySize = -1, #Preview [string]$Preview, [string]$PreviewWindow, # Scripting [Alias('q')] [string]$Query, [Alias('s1')] [switch]$Select1, [Alias('e0')] [switch]$Exit0, [Alias('f')] [string]$Filter, [switch]$PrintQuery, [string]$Expect, [Parameter(ValueFromPipeline=$True)] [object[]]$Input ) Begin { # process parameters: $arguments = '' if ($PSBoundParameters.ContainsKey('Extended') -and $Extended) { $arguments += '--extended '} if ($PSBoundParameters.ContainsKey('Exact') -and $Exact) { $arguments += '--exact '} if ($PSBoundParameters.ContainsKey('CaseInsensitive') -and $CaseInsensitive) { $arguments += '-i '} if ($PSBoundParameters.ContainsKey('CaseSensitive') -and $CaseSensitive) { $arguments += '+i '} if ($PSBoundParameters.ContainsKey('Delimiter') -and ![string]::IsNullOrWhiteSpace($Delimiter)) { $arguments += "--delimiter=$Delimiter "} if ($PSBoundParameters.ContainsKey('NoSort') -and $NoSort) { $arguments += '--no-sort '} if ($PSBoundParameters.ContainsKey('ReverseInput') -and $ReverseInput) { $arguments += '--tac '} if ($PSBoundParameters.ContainsKey('Phony') -and $Phony) { $arguments += '--phony '} if ($PSBoundParameters.ContainsKey('Tiebreak') -and ![string]::IsNullOrWhiteSpace($Tiebreak)) { $arguments += "--tiebreak=$Tiebreak "} if ($PSBoundParameters.ContainsKey('Multi') -and $Multi) { $arguments += '--multi '} if ($PSBoundParameters.ContainsKey('NoMouse') -and $NoMouse) { $arguments += '--no-mouse '} if ($PSBoundParameters.ContainsKey('Bind') -and $Bind.Length -ge 1) { $Bind | ForEach-Object { $arguments += "--bind=""$_"" " } } if ($PSBoundParameters.ContainsKey('Reverse') -and $Reverse) { $arguments += '--reverse '} if ($PSBoundParameters.ContainsKey('Cycle') -and $Cycle) { $arguments += '--cycle '} if ($PSBoundParameters.ContainsKey('KeepRight') -and $KeepRight) { $arguments += '--keep-right '} if ($PSBoundParameters.ContainsKey('NoHScroll') -and $NoHScroll) { $arguments += '--no-hscroll '} if ($PSBoundParameters.ContainsKey('FilepathWord') -and $FilepathWord) { $arguments += '--filepath-word '} if ($PSBoundParameters.ContainsKey('Height') -and ![string]::IsNullOrWhiteSpace($Height)) { $arguments += "--height=$height "} if ($PSBoundParameters.ContainsKey('MinHeight') -and $MinHeight -ge 0) { $arguments += "--min-height=$MinHeight "} if ($PSBoundParameters.ContainsKey('Layout') -and ![string]::IsNullOrWhiteSpace($Layout)) { $arguments += "--layout=$Layout "} if ($PSBoundParameters.ContainsKey('Border') -and $Border) { $arguments += '--border '} if ($PSBoundParameters.ContainsKey('BorderStyle') -and ![string]::IsNullOrWhiteSpace($BorderStyle)) { $arguments += "--border=$BorderStyle "} if ($PSBoundParameters.ContainsKey('Info') -and ![string]::IsNullOrWhiteSpace($Info)) { $arguments += "--info=$Info "} if ($PSBoundParameters.ContainsKey('Prompt') -and ![string]::IsNullOrWhiteSpace($Prompt)) { $arguments += "--prompt=""$Prompt"" "} if ($PSBoundParameters.ContainsKey('Pointer') -and ![string]::IsNullOrWhiteSpace($Pointer)) { $arguments += "--pointer=""$Pointer"" "} if ($PSBoundParameters.ContainsKey('Marker') -and ![string]::IsNullOrWhiteSpace($Marker)) { $arguments += "--marker=""$Marker"" "} if ($PSBoundParameters.ContainsKey('Header') -and ![string]::IsNullOrWhiteSpace($Header)) { $arguments += "--header=""$Header"" "} if ($PSBoundParameters.ContainsKey('HeaderLines') -and $HeaderLines -ge 0) { $arguments += "--header-lines=$HeaderLines "} if ($PSBoundParameters.ContainsKey('Ansi') -and $Ansi) { $arguments += '--ansi '} if ($PSBoundParameters.ContainsKey('Tabstop') -and $Tabstop -ge 0) { $arguments += "--tabstop=$Tabstop "} if ($PSBoundParameters.ContainsKey('Color') -and ![string]::IsNullOrWhiteSpace($Color)) { $arguments += "--color=""$Color"" "} if ($PSBoundParameters.ContainsKey('NoBold') -and $NoBold) { $arguments += '--no-bold '} if ($PSBoundParameters.ContainsKey('History') -and $History) { $arguments += "--history=""$History"" "} if ($PSBoundParameters.ContainsKey('HistorySize') -and $HistorySize -ge 1) { $arguments += "--history-size=$HistorySize "} if ($PSBoundParameters.ContainsKey('Preview') -and ![string]::IsNullOrWhiteSpace($Preview)) { $arguments += "--preview=""$Preview"" "} if ($PSBoundParameters.ContainsKey('PreviewWindow') -and ![string]::IsNullOrWhiteSpace($PreviewWindow)) { $arguments += "--preview-window=""$PreviewWindow"" "} if ($PSBoundParameters.ContainsKey('Query') -and ![string]::IsNullOrWhiteSpace($Query)) { $arguments += "--query=""{0}"" " -f $(PrepareArg $Query)} if ($PSBoundParameters.ContainsKey('Select1') -and $Select1) { $arguments += '--select-1 '} if ($PSBoundParameters.ContainsKey('Exit0') -and $Exit0) { $arguments += '--exit-0 '} if ($PSBoundParameters.ContainsKey('Filter') -and ![string]::IsNullOrEmpty($Filter)) { $arguments += "--filter=$Filter " } if ($PSBoundParameters.ContainsKey('PrintQuery') -and $PrintQuery) { $arguments += '--print-query '} if ($PSBoundParameters.ContainsKey('Expect') -and ![string]::IsNullOrWhiteSpace($Expect)) { $arguments += "--expect=""$Expect"" "} if (!$script:OverrideFzfDefaults) { $script:OverrideFzfDefaults = [FzfDefaultOpts]::new("") } if ($script:UseHeightOption -and [string]::IsNullOrWhiteSpace($Height) -and ` ([string]::IsNullOrWhiteSpace($script:OverrideFzfDefaults.Get()) -or ` (-not $script:OverrideFzfDefaults.Get().Contains('--height')))) { $arguments += "--height=40% " } if ($Border -eq $true -and -not [string]::IsNullOrWhiteSpace($BorderStyle)) { throw '-Border and -BorderStyle are mutally exclusive' } if ($script:UseFd -and $script:RunningInWindowsTerminal -and -not $arguments.Contains('--ansi')) { $arguments += "--ansi " } # prepare to start process: $process = New-Object System.Diagnostics.Process $process.StartInfo.FileName = $script:FzfLocation $process.StartInfo.Arguments = $arguments $process.StartInfo.StandardOutputEncoding = [System.Text.Encoding]::UTF8 $process.StartInfo.RedirectStandardInput = $true $process.StartInfo.RedirectStandardOutput = $true $process.StartInfo.UseShellExecute = $false if ($pwd.Provider.Name -eq 'FileSystem') { $process.StartInfo.WorkingDirectory = $pwd.ProviderPath } # Adding event handers for stdout: $stdOutEventId = "PsFzfStdOutEh-" + [System.Guid]::NewGuid() $stdOutEvent = Register-ObjectEvent -InputObject $process ` -EventName 'OutputDataReceived' ` -SourceIdentifier $stdOutEventId $processHasExited = new-object psobject -property @{flag = $false} # register on exit: $scriptBlockExited = { $Event.MessageData.flag = $true } $exitedEventId = "PsFzfExitedEh-" + [System.Guid]::NewGuid() $exitedEvent = Register-ObjectEvent -InputObject $process ` -Action $scriptBlockExited -EventName 'Exited' ` -SourceIdentifier $exitedEventId ` -MessageData $processHasExited $process.Start() | Out-Null $process.BeginOutputReadLine() | Out-Null $utf8Encoding = New-Object System.Text.UTF8Encoding -ArgumentList $false $script:utf8Stream = New-Object System.IO.StreamWriter -ArgumentList $process.StandardInput.BaseStream, $utf8Encoding $cleanup = [scriptblock] { if ($script:OverrideFzfDefaults) { $script:OverrideFzfDefaults.Restore() $script:OverrideFzfDefaults = $null } try { $process.StandardInput.Close() | Out-Null $process.WaitForExit() } catch { # do nothing } try { #$stdOutEventId,$exitedEventId | ForEach-Object { # Unregister-Event $_ -ErrorAction SilentlyContinue #} $stdOutEvent,$exitedEvent | ForEach-Object { Stop-Job $_ -ErrorAction SilentlyContinue Remove-Job $_ -Force -ErrorAction SilentlyContinue } } catch { } # events seem to be generated out of order - therefore, we need sort by time created. For example, # -print-query and -expect and will be outputted first if specified on the command line. Get-Event -SourceIdentifier $stdOutEventId | ` Sort-Object -Property TimeGenerated | ` Where-Object { $null -ne $_.SourceEventArgs.Data } | ForEach-Object { Write-Output $_.SourceEventArgs.Data Remove-Event -EventIdentifier $_.EventIdentifier } } $checkProcessStatus = [scriptblock] { if ($processHasExited.flag -or $process.HasExited) { $script:utf8stream = $null & $cleanup Stop-Pipeline } } } Process { $hasInput = $PSBoundParameters.ContainsKey('Input') # handle no piped input: if (!$hasInput) { # optimization for filesystem provider: if ($PWD.Provider.Name -eq 'FileSystem') { Invoke-Expression (Get-FileSystemCmd $PWD.ProviderPath) | ForEach-Object { try { $utf8Stream.WriteLine($_) } catch [System.Management.Automation.MethodInvocationException] { # Possibly broken pipe. Next clause will handle graceful shutdown. } finally { & $checkProcessStatus } } } else { Get-ChildItem . -Recurse -ErrorAction SilentlyContinue | ForEach-Object { $item = $_ if ($item -is [System.String]) { $str = $item } else { # search through common properties: $str = $item.FullName if ($null -eq $str) { $str = $item.Name if ($null -eq $str) { $str = $item.ToString() } } } try { $utf8Stream.WriteLine($str) } catch [System.Management.Automation.MethodInvocationException] { # Possibly broken pipe. We will shutdown the pipe below. } & $checkProcessStatus } } } else { foreach ($item in $Input) { if ($item -is [System.String]) { $str = $item } else { # search through common properties: $str = $item.FullName if ($null -eq $str) { $str = $item.Name if ($null -eq $str) { $str = $item.ToString() } } } try { $utf8Stream.WriteLine($str) } catch [System.Management.Automation.MethodInvocationException] { # Possibly broken pipe. We will shutdown the pipe below. } & $checkProcessStatus } } if ($null -ne $utf8Stream) { try { $utf8Stream.Flush() } catch [System.Management.Automation.MethodInvocationException] { # Possibly broken pipe, check process status. & $checkProcessStatus } } } End { & $cleanup } } function Find-CurrentPath { param([string]$line,[int]$cursor,[ref]$leftCursor,[ref]$rightCursor) if ($line.Length -eq 0) { $leftCursor.Value = $rightCursor.Value = 0 return $null } if ($cursor -ge $line.Length) { $leftCursorTmp = $cursor - 1 } else { $leftCursorTmp = $cursor } :leftSearch for (;$leftCursorTmp -ge 0;$leftCursorTmp--) { if ([string]::IsNullOrWhiteSpace($line[$leftCursorTmp])) { if (($leftCursorTmp -lt $cursor) -and ($leftCursorTmp -lt $line.Length-1)) { $leftCursorTmpQuote = $leftCursorTmp - 1 $leftCursorTmp = $leftCursorTmp + 1 } else { $leftCursorTmpQuote = $leftCursorTmp } for (;$leftCursorTmpQuote -ge 0;$leftCursorTmpQuote--) { if (($line[$leftCursorTmpQuote] -eq '"') -and (($leftCursorTmpQuote -le 0) -or ($line[$leftCursorTmpQuote-1] -ne '"'))) { $leftCursorTmp = $leftCursorTmpQuote break leftSearch } elseif (($line[$leftCursorTmpQuote] -eq "'") -and (($leftCursorTmpQuote -le 0) -or ($line[$leftCursorTmpQuote-1] -ne "'"))) { $leftCursorTmp = $leftCursorTmpQuote break leftSearch } } break leftSearch } } :rightSearch for ($rightCursorTmp = $cursor;$rightCursorTmp -lt $line.Length;$rightCursorTmp++) { if ([string]::IsNullOrWhiteSpace($line[$rightCursorTmp])) { if ($rightCursorTmp -gt $cursor) { $rightCursorTmp = $rightCursorTmp - 1 } for ($rightCursorTmpQuote = $rightCursorTmp+1;$rightCursorTmpQuote -lt $line.Length;$rightCursorTmpQuote++) { if (($line[$rightCursorTmpQuote] -eq '"') -and (($rightCursorTmpQuote -gt $line.Length) -or ($line[$rightCursorTmpQuote+1] -ne '"'))) { $rightCursorTmp = $rightCursorTmpQuote break rightSearch } elseif (($line[$rightCursorTmpQuote] -eq "'") -and (($rightCursorTmpQuote -gt $line.Length) -or ($line[$rightCursorTmpQuote+1] -ne "'"))) { $rightCursorTmp = $rightCursorTmpQuote break rightSearch } } break rightSearch } } if ($leftCursorTmp -lt 0 -or $leftCursorTmp -gt $line.Length-1) { $leftCursorTmp = 0} if ($rightCursorTmp -ge $line.Length) { $rightCursorTmp = $line.Length-1 } $leftCursor.Value = $leftCursorTmp $rightCursor.Value = $rightCursorTmp $str = -join ($line[$leftCursorTmp..$rightCursorTmp]) return $str.Trim("'").Trim('"') } function Invoke-FzfDefaultSystem { param($ProviderPath,$DefaultOpts) $script:OverrideFzfDefaultOpts = [FzfDefaultOpts]::new($DefaultOpts) $arguments = '' if (-not $script:OverrideFzfDefaultOpts.Get().Contains('--height')) { $arguments += "--height=40% " } if ($script:UseFd -and $script:RunningInWindowsTerminal -and -not $script:OverrideFzfDefaultOpts.Get().Contains('--ansi')) { $arguments += "--ansi " } $script:OverrideFzfDefaultCommand = [FzfDefaultCmd]::new('') try { # native filesystem walking is MUCH faster with native Go code: $env:FZF_DEFAULT_COMMAND = "" $result = @() # --height doesn't work with Invoke-Expression - not sure why. Thus, we need to use # System.Diagnostics.Process: $process = New-Object System.Diagnostics.Process $process.StartInfo.FileName = $script:FzfLocation $process.StartInfo.Arguments = $arguments $process.StartInfo.RedirectStandardInput = $false $process.StartInfo.RedirectStandardOutput = $true $process.StartInfo.UseShellExecute = $false $process.StartInfo.WorkingDirectory = $ProviderPath # Adding event handers for stdout: $stdOutEventId = "Invoke-FzfDefaultSystem-PsFzfStdOutEh-" + [System.Guid]::NewGuid() $stdOutEvent = Register-ObjectEvent -InputObject $process ` -EventName 'OutputDataReceived' ` -SourceIdentifier $stdOutEventId $process.Start() | Out-Null $process.BeginOutputReadLine() | Out-Null $process.WaitForExit() Get-Event -SourceIdentifier $stdOutEventId | ` Sort-Object -Property TimeGenerated | ` Where-Object { $null -ne $_.SourceEventArgs.Data } | ForEach-Object { $result += $_.SourceEventArgs.Data Remove-Event -EventIdentifier $_.EventIdentifier } Remove-Event -SourceIdentifier $stdOutEventId } catch { # ignore errors } finally { if ($script:OverrideFzfDefaultCommand) { $script:OverrideFzfDefaultCommand.Restore() $script:OverrideFzfDefaultCommand = $null } if ($script:OverrideFzfDefaultOpts) { $script:OverrideFzfDefaultOpts.Restore() $script:OverrideFzfDefaultOpts = $null } } return $result } function Invoke-FzfPsReadlineHandlerProvider { $leftCursor = $null $rightCursor = $null $line = $null $cursor = $null [Microsoft.PowerShell.PSConsoleReadline]::GetBufferState([ref]$line, [ref]$cursor) $currentPath = Find-CurrentPath $line $cursor ([ref]$leftCursor) ([ref]$rightCursor) $addSpace = $null -ne $currentPath -and $currentPath.StartsWith(" ") if ([String]::IsNullOrWhitespace($currentPath) -or !(Test-Path $currentPath)) { $currentPath = $PWD } $result = @() try { $script:OverrideFzfDefaults = [FzfDefaultOpts]::new($env:FZF_CTRL_T_OPTS) if (-not [System.String]::IsNullOrWhiteSpace($env:FZF_CTRL_T_COMMAND)) { Invoke-Expression ($env:FZF_CTRL_T_COMMAND) | Invoke-Fzf -Multi | ForEach-Object { $result += $_ } } else { if ([string]::IsNullOrWhiteSpace($currentPath)) { Invoke-Fzf -Multi | ForEach-Object { $result += $_ } } else { $resolvedPath = Resolve-Path $currentPath -ErrorAction SilentlyContinue $providerName = $null if ($null -ne $resolvedPath) { $providerName = $resolvedPath.Provider.Name } switch ($providerName) { # Get-ChildItem is way too slow - we optimize using our own function for calling fzf directly (Invoke-FzfDefaultSystem): 'FileSystem' { if (-not $script:UseFd) { $result = Invoke-FzfDefaultSystem $resolvedPath.ProviderPath '--multi' } else { Invoke-Expression (Get-FileSystemCmd $resolvedPath.ProviderPath) | Invoke-Fzf -Multi | ForEach-Object { $result += $_ } } } 'Registry' { Get-ChildItem $currentPath -Recurse -ErrorAction SilentlyContinue | Select-Object Name -ExpandProperty Name | Invoke-Fzf -Multi | ForEach-Object { $result += $_ } } $null { Get-ChildItem $currentPath -Recurse -ErrorAction SilentlyContinue | Select-Object FullName -ExpandProperty FullName | Invoke-Fzf -Multi | ForEach-Object { $result += $_ } } Default {} } } } } catch { # catch custom exception } finally { if ($script:OverrideFzfDefaults) { $script:OverrideFzfDefaults.Restore() $script:OverrideFzfDefaults = $null } } InvokePromptHack if ($null -ne $result) { # quote strings if we need to: if ($result -is [system.array]) { for ($i = 0;$i -lt $result.Length;$i++) { $result[$i] = FixCompletionResult $result[$i] } } else { $result = FixCompletionResult $result } $str = $result -join ',' if ($addSpace) { $str = ' ' + $str } $replaceLen = $rightCursor - $leftCursor if ($rightCursor -eq 0 -and $leftCursor -eq 0) { [Microsoft.PowerShell.PSConsoleReadLine]::Insert($str) } else { [Microsoft.PowerShell.PSConsoleReadLine]::Replace($leftCursor,$replaceLen+1,$str) } } } function Invoke-FzfPsReadlineHandlerHistory { $result = $null try { $script:OverrideFzfDefaults = [FzfDefaultOpts]::new($env:FZF_CTRL_R_OPTS) $line = $null $cursor = $null [Microsoft.PowerShell.PSConsoleReadline]::GetBufferState([ref]$line, [ref]$cursor) $reader = New-Object PSFzf.IO.ReverseLineReader -ArgumentList $((Get-PSReadlineOption).HistorySavePath) $fileHist = @{} $reader.GetEnumerator() | ForEach-Object { if (-not $fileHist.ContainsKey($_)) { $fileHist.Add($_,$true) $_ } } | Invoke-Fzf -Query "$line" -Bind ctrl-r:toggle-sort | ForEach-Object { $result = $_ } } catch { # catch custom exception } finally { if ($script:OverrideFzfDefaults) { $script:OverrideFzfDefaults.Restore() $script:OverrideFzfDefaults = $null } # ensure that stream is closed: $reader.Dispose() } InvokePromptHack if (-not [string]::IsNullOrEmpty($result)) { [Microsoft.PowerShell.PSConsoleReadLine]::Replace(0,$line.Length,$result) } } function Invoke-FzfPsReadlineHandlerHistoryArgs { $result = @() try { $line = $null $cursor = $null [Microsoft.PowerShell.PSConsoleReadline]::GetBufferState([ref]$line, [ref]$cursor) $line = $line.Insert($cursor,"{}") # add marker for fzf $contentTable = @{} $reader = New-Object PSFzf.IO.ReverseLineReader -ArgumentList $((Get-PSReadlineOption).HistorySavePath) $fileHist = @{} $reader.GetEnumerator() | ForEach-Object { if (-not $fileHist.ContainsKey($_)) { $fileHist.Add($_,$true) [System.Management.Automation.PsParser]::Tokenize($_, [ref] $null) } } | Where-Object {$_.type -eq "commandargument" -or $_.type -eq "string"} | ForEach-Object { if (!$contentTable.ContainsKey($_.Content)) { $_.Content ; $contentTable[$_.Content] = $true } } | Invoke-Fzf -NoSort -Multi | ForEach-Object { $result += $_ } } catch { # catch custom exception } finally { $reader.Dispose() } InvokePromptHack [array]$result = $result | ForEach-Object { # add quotes: if ($_.Contains(" ") -or $_.Contains("`t")) { "'{0}'" -f $_.Replace("'","''") } else { $_ } } if ($result.Length -ge 0) { [Microsoft.PowerShell.PSConsoleReadLine]::Replace($cursor,0,($result -join ' ')) } } function Invoke-FzfPsReadlineHandlerSetLocation { $result = $null try { $script:OverrideFzfDefaults = [FzfDefaultOpts]::new($env:FZF_ALT_C_OPTS) if ($null -eq $env:FZF_ALT_C_COMMAND) { Invoke-Expression (Get-FileSystemCmd . -dirOnly) | Invoke-Fzf | ForEach-Object { $result = $_ } } else { Invoke-Expression ($env:FZF_ALT_C_COMMAND) | Invoke-Fzf | ForEach-Object { $result = $_ } } } catch { # catch custom exception } finally { if ($script:OverrideFzfDefaults) { $script:OverrideFzfDefaults.Restore() $script:OverrideFzfDefaults = $null } } if (-not [string]::IsNullOrEmpty($result)) { & $script:AltCCommand -Location $result [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() } else { InvokePromptHack } } function SetPsReadlineShortcut($Chord,[switch]$Override,$BriefDesc,$Desc,[scriptblock]$scriptBlock) { if ([string]::IsNullOrEmpty($Chord)) { return $false } if ((Get-PSReadlineKeyHandler -Bound | Where-Object {$_.Key.ToLower() -eq $Chord}) -and -not $Override) { return $false } else { Set-PSReadlineKeyHandler -Key $Chord -Description $Desc -BriefDescription $BriefDesc -ScriptBlock $scriptBlock return $true } } function FindFzf() { if ($script:IsWindows) { $AppNames = @('fzf-*-windows_*.exe','fzf.exe') } else { if ($IsMacOS) { $AppNames = @('fzf-*-darwin_*','fzf') } elseif ($IsLinux) { $AppNames = @('fzf-*-linux_*','fzf') } else { throw 'Unknown OS' } } # find it in our path: $script:FzfLocation = $null $AppNames | ForEach-Object { if ($null -eq $script:FzfLocation) { $result = Get-Command $_ -ErrorAction SilentlyContinue $result | ForEach-Object { $script:FzfLocation = Resolve-Path $_.Source } } } if ($null -eq $script:FzfLocation) { throw 'Failed to find fzf binary in PATH. You can download a binary from this page: https://github.com/junegunn/fzf/releases' } } $PsReadlineShortcuts = @{ PSReadlineChordProvider = [PSCustomObject]@{ 'Chord' = "$PSReadlineChordProvider" 'BriefDesc' = 'Fzf Provider Select' 'Desc' = 'Run fzf for current provider based on current token' 'ScriptBlock' = { Invoke-FzfPsReadlineHandlerProvider } }; PSReadlineChordReverseHistory = [PsCustomObject]@{ 'Chord' = "$PSReadlineChordReverseHistory" 'BriefDesc' = 'Fzf Reverse History Select' 'Desc' = 'Run fzf to search through PSReadline history' 'ScriptBlock' = { Invoke-FzfPsReadlineHandlerHistory } }; PSReadlineChordSetLocation = @{ 'Chord' = "$PSReadlineChordSetLocation" 'BriefDesc' = 'Fzf Set Location' 'Desc' = 'Run fzf to select directory to set current location' 'ScriptBlock' = { Invoke-FzfPsReadlineHandlerSetLocation } }; PSReadlineChordReverseHistoryArgs = @{ 'Chord' = "$PSReadlineChordReverseHistoryArgs" 'BriefDesc' = 'Fzf Reverse History Arg Select' 'Desc' = 'Run fzf to search through command line arguments in PSReadline history' 'ScriptBlock' = { Invoke-FzfPsReadlineHandlerHistoryArgs } }; PSReadlineChordTabCompletion = [PSCustomObject]@{ 'Chord' = "Tab" 'BriefDesc' = 'Fzf Tab Completion' 'Desc' = 'Invoke Fzf for tab completion' 'ScriptBlock' = { Invoke-TabCompletion } }; } if (Get-Module -ListAvailable -Name PSReadline) { $PsReadlineShortcuts.GetEnumerator() | ForEach-Object { $info = $_.Value $result = SetPsReadlineShortcut $info.Chord -Override:$PSBoundParameters.ContainsKey($_.Key) $info.BriefDesc $info.Desc $info.ScriptBlock # store that the chord is not activated: if (-not $result) { $info.Chord = $null } } } else { Write-Warning "PSReadline module not found - keyboard handlers not installed" } FindFzf try { $fzfVersion = $(& $script:FzfLocation --version).Replace(' (devel)','').Split('.') $script:UseHeightOption = $fzfVersion.length -ge 2 -and ` ([int]$fzfVersion[0] -gt 0 -or ` [int]$fzfVersion[1] -ge 21) -and ` $script:RunningInWindowsTerminal } catch { # continue } # check if we're running on Windows PowerShell. This method is faster than Get-Command: if ($(get-host).Version.Major -le 5) { $script:PowershellCmd = 'powershell' } else { $script:PowershellCmd = 'pwsh' } # PSFzf.Functions.ps1 #.ExternalHelp PSFzf.psm1-help.xml $addedAliases = @() function script:SetPsFzfAlias { param($Name, $Function) New-Alias -Name $Name -Scope Global -Value $Function -ErrorAction Ignore $addedAliases += $Name } function script:SetPsFzfAliasCheck { param($Name, $Function) # prevent Get-Command from loading PSFzf $script:PSModuleAutoLoadingPreferencePrev = $PSModuleAutoLoadingPreference $PSModuleAutoLoadingPreference = 'None' if (-not (Get-Command -Name $Name -ErrorAction Ignore)) { SetPsFzfAlias $Name $Function } # restore module auto loading $PSModuleAutoLoadingPreference = $script:PSModuleAutoLoadingPreferencePrev } function script:RemovePsFzfAliases { $addedAliases | ForEach-Object { Remove-Item -Path Alias:$_ } } function Get-EditorLaunch() { param($FileList, $LineNum = 0) # HACK to check to see if we're running under Visual Studio Code. # If so, reuse Visual Studio Code currently open windows: $editorOptions = '' if (-not [string]::IsNullOrEmpty($env:PSFZF_EDITOR_OPTIONS)) { $editorOptions += ' ' + $env:PSFZF_EDITOR_OPTIONS } if ($null -ne $env:VSCODE_PID) { $editor = 'code' $editorOptions += ' --reuse-window' } else { $editor = if ($ENV:VISUAL) { $ENV:VISUAL }elseif ($ENV:EDITOR) { $ENV:EDITOR } if ($null -eq $editor) { if (!$IsWindows) { $editor = 'vim' } else { $editor = 'code' } } } if ($editor -eq 'code') { if ($FileList -is [array] -and $FileList.length -gt 1) { for ($i = 0; $i -lt $FileList.Count; $i++) { $FileList[$i] = '"{0}"' -f $(Resolve-Path $FileList[$i].Trim('"')) } "$editor$editorOptions {0}" -f ($FileList -join ' ') } else { "$editor$editorOptions --goto ""{0}:{1}""" -f $(Resolve-Path $FileList.Trim('"')), $LineNum } } elseif ($editor -match '[gn]?vi[m]?') { if ($FileList -is [array] -and $FileList.length -gt 1) { for ($i = 0; $i -lt $FileList.Count; $i++) { $FileList[$i] = '"{0}"' -f $(Resolve-Path $FileList[$i].Trim('"')) } "$editor$editorOptions {0}" -f ($FileList -join ' ') } else { "$editor$editorOptions ""{0}"" +{1}" -f $(Resolve-Path $FileList.Trim('"')), $LineNum } } elseif ($editor -eq 'nano') { if ($FileList -is [array] -and $FileList.length -gt 1) { for ($i = 0; $i -lt $FileList.Count; $i++) { $FileList[$i] = '"{0}"' -f $(Resolve-Path $FileList[$i].Trim('"')) } "$editor$editorOptions {0}" -f ($FileList -join ' ') } else { "$editor$editorOptions +{1} {0}" -f $(Resolve-Path $FileList.Trim('"')), $LineNum } } } function Invoke-FuzzyEdit() { param($Directory = ".") $files = @() try { if ( Test-Path $Directory) { if ( (Get-Item $Directory).PsIsContainer ) { $prevDir = $PWD.ProviderPath cd $Directory Invoke-Expression (Get-FileSystemCmd .) | Invoke-Fzf -Multi | ForEach-Object { $files += "$_" } } else { $files += $Directory $Directory = Split-Path -Parent $Directory } } } catch { } finally { if ($prevDir) { cd $prevDir } } if ($files.Count -gt 0) { try { if ($Directory) { $prevDir = $PWD.Path cd $Directory } # Not sure if being passed relative or absolute path $cmd = Get-EditorLaunch -FileList $files Write-Host "Executing '$cmd'..." Invoke-Expression -Command $cmd } catch { } finally { if ($prevDir) { cd $prevDir } } } } #.ExternalHelp PSFzf.psm1-help.xml function Invoke-FuzzyFasd() { $result = $null try { if (Get-Command Get-Frecents -ErrorAction Ignore) { Get-Frecents | ForEach-Object { $_.FullPath } | Invoke-Fzf -ReverseInput -NoSort | ForEach-Object { $result = $_ } } elseif (Get-Command fasd -ErrorAction Ignore) { fasd -l | Invoke-Fzf -ReverseInput -NoSort | ForEach-Object { $result = $_ } } } catch { } if ($null -ne $result) { # use cd in case it's aliased to something else: cd $result } } #.ExternalHelp PSFzf.psm1-help.xml function Invoke-FuzzyHistory() { if (Get-Command Get-PSReadLineOption -ErrorAction SilentlyContinue) { $result = Get-Content (Get-PSReadLineOption).HistorySavePath | Invoke-Fzf -Reverse -NoSort } else { $result = Get-History | ForEach-Object { $_.CommandLine } | Invoke-Fzf -Reverse -NoSort } if ($null -ne $result) { Write-Output "Invoking '$result'`n" Invoke-Expression "$result" -Verbose } } # needs to match helpers/GetProcessesList.ps1 function GetProcessesList() { Get-Process | ` Where-Object { ![string]::IsNullOrEmpty($_.ProcessName) } | ` ForEach-Object { $pmSize = $_.PM / 1MB $cpu = $_.CPU # make sure we display a value so we can correctly parse selections: if ($null -eq $cpu) { $cpu = 0.0 } "{0,-8:n2} {1,-8:n2} {2,-8} {3}" -f $pmSize, $cpu, $_.Id, $_.ProcessName } } function GetProcessSelection() { param( [scriptblock] $ResultAction ) $previewScript = $(Join-Path $PsScriptRoot 'helpers/GetProcessesList.ps1') $cmd = $($script:PowershellCmd + " -NoProfile -NonInteractive -File \""$previewScript\""") $header = "`n" + ` "`nCTRL+R-Reload`tCTRL+A-Select All`tCTRL+D-Deselect All`tCTRL+T-Toggle All`n`n" + ` $("{0,-8} {1,-8} {2,-8} PROCESS NAME" -f "PM(M)", "CPU", "ID") + "`n" + ` "{0,-8} {1,-8} {2,-8} {3,-12}" -f "-----", "---", "--", "------------" $result = GetProcessesList | ` Invoke-Fzf -Multi -Header $header ` -Bind "ctrl-r:reload($cmd)", "ctrl-a:select-all", "ctrl-d:deselect-all", "ctrl-t:toggle-all" ` -Preview "echo {}" -PreviewWindow """down,3,wrap""" ` -Layout reverse -Height 80% $result | ForEach-Object { &$ResultAction $_ } } #.ExternalHelp PSFzf.psm1-help.xml function Invoke-FuzzyKillProcess() { GetProcessSelection -ResultAction { param($result) $resultSplit = $result.split(' ', [System.StringSplitOptions]::RemoveEmptyEntries) $processIdIdx = 2 $id = $resultSplit[$processIdIdx] Stop-Process -Id $id -Verbose } } #.ExternalHelp PSFzf.psm1-help.xml function Invoke-FuzzySetLocation() { param($Directory = $null) if ($null -eq $Directory) { $Directory = $PWD.ProviderPath } $result = $null try { if ([string]::IsNullOrWhiteSpace($env:FZF_DEFAULT_COMMAND)) { Get-ChildItem $Directory -Recurse -ErrorAction Ignore | Where-Object { $_.PSIsContainer } | Invoke-Fzf | ForEach-Object { $result = $_ } } else { Invoke-Fzf | ForEach-Object { $result = $_ } } } catch { } if ($null -ne $result) { Set-Location $result } } if ((-not $IsLinux) -and (-not $IsMacOS)) { #.ExternalHelp PSFzf.psm1-help.xml function Set-LocationFuzzyEverything() { param($Directory = $null) if ($null -eq $Directory) { $Directory = $PWD.ProviderPath $Global = $False } else { $Global = $True } $result = $null try { Search-Everything -Global:$Global -PathInclude $Directory -FolderInclude @('') | Invoke-Fzf | ForEach-Object { $result = $_ } } catch { } if ($null -ne $result) { # use cd in case it's aliased to something else: cd $result } } } #.ExternalHelp PSFzf.psm1-help.xml function Invoke-FuzzyZLocation() { $result = $null try { (Get-ZLocation).GetEnumerator() | Sort-Object { $_.Value } -Descending | ForEach-Object { $_.Key } | Invoke-Fzf -NoSort | ForEach-Object { $result = $_ } } catch { } if ($null -ne $result) { # use cd in case it's aliased to something else: cd $result } } #.ExternalHelp PSFzf.psm1-help.xml function Invoke-FuzzyScoop() { param( [string]$subcommand = "install", [string]$subcommandflags = "" ) $result = $null $scoopexists = Get-Command scoop -ErrorAction Ignore if ($scoopexists) { $apps = New-Object System.Collections.ArrayList Get-ChildItem "$(Split-Path $scoopexists.Path)\..\buckets" | ForEach-Object { $bucket = $_.Name Get-ChildItem "$($_.FullName)\bucket" | ForEach-Object { $apps.Add($bucket + '/' + ($_.Name -replace '.json', '')) > $null } } $result = $apps | Invoke-Fzf -Header "Scoop Applications" -Multi -Preview "scoop info {}" -PreviewWindow wrap } if ($null -ne $result) { Invoke-Expression "scoop $subcommand $($result -join ' ') $subcommandflags" } } #.ExternalHelp PSFzf.psm1-help.xml function Invoke-FuzzyGitStatus() { Invoke-PsFzfGitFiles } function Invoke-PsFzfRipgrep() { # this function is adapted from https://github.com/junegunn/fzf/blob/master/ADVANCED.md#switching-between-ripgrep-mode-and-fzf-mode param([Parameter(Mandatory)]$SearchString, [switch]$NoEditor) $RG_PREFIX = "rg --column --line-number --no-heading --color=always --smart-case " $INITIAL_QUERY = $SearchString $script:OverrideFzfDefaultCommand = [FzfDefaultCmd]::new('') try { if ($script:IsWindows) { $sleepCmd = '' $trueCmd = 'cd .' $env:FZF_DEFAULT_COMMAND = "$RG_PREFIX ""$INITIAL_QUERY""" } else { $sleepCmd = 'sleep 0.1;' $trueCmd = 'true' $env:FZF_DEFAULT_COMMAND = '{0} $(printf %q "{1}")' -f $RG_PREFIX, $INITIAL_QUERY } & $script:FzfLocation --ansi ` --color "hl:-1:underline,hl+:-1:underline:reverse" ` --disabled --query "$INITIAL_QUERY" ` --bind "change:reload:$sleepCmd $RG_PREFIX {q} || $trueCmd" ` --bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+clear-query+rebind(ctrl-r)" ` --bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || $trueCmd)+rebind(change,ctrl-f)" ` --prompt '1. Ripgrep> ' ` --delimiter : ` --header '╱ CTRL-R (Ripgrep mode) ╱ CTRL-F (fzf mode) ╱' ` --preview 'bat --color=always {1} --highlight-line {2}' ` --preview-window 'up,60%,border-bottom,+{2}+3/3,~3' | ` ForEach-Object { $results += $_ } if (-not [string]::IsNullOrEmpty($results)) { $split = $results.Split(':') $fileList = $split[0] $lineNum = $split[1] if ($NoEditor) { Resolve-Path $fileList } else { $cmd = Get-EditorLaunch -FileList $fileList -LineNum $lineNum Write-Host "Executing '$cmd'..." Invoke-Expression -Command $cmd } } } catch { Write-Error "Error occurred: $_" } finally { if ($script:OverrideFzfDefaultCommand) { $script:OverrideFzfDefaultCommand.Restore() $script:OverrideFzfDefaultCommand = $null } } } function Enable-PsFzfAliases() { # set aliases: if (-not $DisableAliases) { SetPsFzfAliasCheck "fe" Invoke-FuzzyEdit SetPsFzfAliasCheck "fh" Invoke-FuzzyHistory SetPsFzfAliasCheck "ff" Invoke-FuzzyFasd SetPsFzfAliasCheck "fkill" Invoke-FuzzyKillProcess SetPsFzfAliasCheck "fd" Invoke-FuzzySetLocation if (${function:Set-LocationFuzzyEverything}) { SetPsFzfAliasCheck "cde" Set-LocationFuzzyEverything } SetPsFzfAliasCheck "fz" Invoke-FuzzyZLocation SetPsFzfAliasCheck "fs" Invoke-FuzzyScoop SetPsFzfAliasCheck "fgs" Invoke-FuzzyGitStatus } } # PSFzf.Git.ps1 $script:GitKeyHandlers = @() $script:foundGit = $false $script:bashPath = $null $script:grepPath = $null if ($PSVersionTable.PSEdition -eq 'Core') { $script:pwshExec = "pwsh" } else { $script:pwshExec = "powershell" } function Get-GitFzfArguments() { # take from https://github.com/junegunn/fzf-git.sh/blob/f72ebd823152fa1e9b000b96b71dd28717bc0293/fzf-git.sh#L89 return @{ Ansi = $true Layout = "reverse" Multi = $true Height = '50%' MinHeight = 20 Border = $true Color = 'header:italic:underline' PreviewWindow = 'right,50%,border-left' Bind = @('ctrl-/:change-preview-window(down,50%,border-top|hidden|)') } } function SetupGitPaths() { if (-not $script:foundGit) { if ($IsLinux -or $IsMacOS) { # TODO: not tested on Mac $script:foundGit = $null -ne $(Get-Command git -ErrorAction SilentlyContinue) $script:bashPath = 'bash' $script:grepPath = 'grep' } else { $gitInfo = Get-Command git.exe -ErrorAction SilentlyContinue $script:foundGit = $null -ne $gitInfo if ($script:foundGit) { # Detect if scoop is installed $script:scoopInfo = Get-Command scoop -ErrorAction SilentlyContinue if ($null -ne $script:scoopInfo) { # Detect if git is installed using scoop (using shims) if ((Split-Path $gitInfo.Source -Parent) -eq (Split-Path $script:scoopInfo.Source -Parent)) { # Get the proper git position relative to scoop shims" position $gitInfo = Get-Command "$($gitInfo.Source)\..\..\apps\git\current\bin\git.exe" } } $gitPathLong = Split-Path (Split-Path $gitInfo.Source -Parent) -Parent # hack to get short path: $a = New-Object -ComObject Scripting.FileSystemObject $f = $a.GetFolder($gitPathLong) $script:bashPath = Join-Path $f.ShortPath "bin\bash.exe" $script:bashPath = Resolve-Path $script:bashPath $script:grepPath = Join-Path ${gitPathLong} "usr\bin\grep.exe" } } } return $script:foundGit } function SetGitKeyBindings($enable) { if ($enable) { if (-not $(SetupGitPaths)) { Write-Error "Failed to register git key bindings - git executable not found" return } if (Get-Command Set-PSReadLineKeyHandler -ErrorAction SilentlyContinue) { @('ctrl+g,ctrl+f', 'Select Git files via fzf', { Update-CmdLine $(Invoke-PsFzfGitFiles) }), ` @('ctrl+g,ctrl+h', 'Select Git hashes via fzf', { Update-CmdLine $(Invoke-PsFzfGitHashes) }), ` @('ctrl+g,ctrl+b', 'Select Git branches via fzf', { Update-CmdLine $(Invoke-PsFzfGitBranches) }), ` @('ctrl+g,ctrl+t', 'Select Git tags via fzf', { Update-CmdLine $(Invoke-PsFzfGitTags) }), ` @('ctrl+g,ctrl+s', 'Select Git stashes via fzf', { Update-CmdLine $(Invoke-PsFzfGitStashes) }) ` | ForEach-Object { $script:GitKeyHandlers += $_[0] Set-PSReadLineKeyHandler -Chord $_[0] -Description $_[1] -ScriptBlock $_[2] } } else { Write-Error "Failed to register git key bindings - PSReadLine module not loaded" return } } } function RemoveGitKeyBindings() { $script:GitKeyHandlers | ForEach-Object { Remove-PSReadLineKeyHandler -Chord $_ } } function IsInGitRepo() { git rev-parse HEAD 2>&1 | Out-Null return $? } function Get-ColorAlways($setting = ' --color=always') { if ($RunningInWindowsTerminal -or -not $IsWindowsCheck) { return $setting } else { return '' } } function Get-HeaderStrings() { $header = "CTRL-A (Select all) / CTRL-D (Deselect all) / CTRL-T (Toggle all)" $keyBinds = 'ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all' return $Header, $keyBinds } function Update-CmdLine($result) { InvokePromptHack if ($result.Length -gt 0) { $result = $result -join " " [Microsoft.PowerShell.PSConsoleReadLine]::Insert($result) } } function Invoke-PsFzfGitFiles() { if (-not (IsInGitRepo)) { return } if (-not $(SetupGitPaths)) { Write-Error "git executable could not be found" return } $previewCmd = "${script:bashPath} \""" + $(Join-Path $PsScriptRoot 'helpers/PsFzfGitFiles-Preview.sh') + "\"" {-1}" + $(Get-ColorAlways) + " \""$($pwd.ProviderPath)\""" $result = @() $headerStrings = Get-HeaderStrings $gitCmdsHeader = "`nALT-S (Git add) / ALT-R (Git reset)" $headerStr = $headerStrings[0] + $gitCmdsHeader + "`n`n" $statusCmd = "git $(Get-ColorAlways '-c color.status=always') status --short" $reloadBindCmd = "reload($statusCmd)" $stageScriptPath = Join-Path $PsScriptRoot 'helpers/PsFzfGitFiles-GitAdd.sh' $gitStageBind = "alt-s:execute-silent(" + """${script:bashPath}"" '${stageScriptPath}' {+2..})+down+${reloadBindCmd}" $resetScriptPath = Join-Path $PsScriptRoot 'helpers/PsFzfGitFiles-GitReset.sh' $gitResetBind = "alt-r:execute-silent(" + """${script:bashPath}"" '${resetScriptPath}' {+2..})+down+${reloadBindCmd}" $fzfArguments = Get-GitFzfArguments $fzfArguments['Bind'] += $headerStrings[1], $gitStageBind, $gitResetBind Invoke-Expression "& $statusCmd" | ` Invoke-Fzf @fzfArguments ` -Prompt '📁 Files> ' ` -Preview "$previewCmd" -Header $headerStr | ` foreach-object { $result += $_.Substring('?? '.Length) } $result } function Invoke-PsFzfGitHashes() { if (-not (IsInGitRepo)) { return } if (-not $(SetupGitPaths)) { Write-Error "git executable could not be found" return } $previewCmd = "${script:bashPath} \""" + $(Join-Path $PsScriptRoot 'helpers/PsFzfGitHashes-Preview.sh') + "\"" {}" + $(Get-ColorAlways) + " \""$pwd\""" $result = @() $fzfArguments = Get-GitFzfArguments & git log --date=short --format="%C(green)%C(bold)%cd %C(auto)%h%d %s (%an)" $(Get-ColorAlways).Trim() --graph | ` Invoke-Fzf @fzfArguments -NoSort ` -Prompt '🍡 Hashes> ' ` -Preview "$previewCmd" | ForEach-Object { if ($_ -match '\d\d-\d\d-\d\d\s+([a-f0-9]+)\s+') { $result += $Matches.1 } } $result } function Invoke-PsFzfGitBranches() { if (-not (IsInGitRepo)) { return } if (-not $(SetupGitPaths)) { Write-Error "git executable could not be found" return } $fzfArguments = Get-GitFzfArguments $fzfArguments['PreviewWindow'] = 'down,border-top,40%' $fzfArguments['Bind'] += 'ctrl-/:change-preview-window(down,70%|hidden|)' $previewCmd = "${script:bashPath} \""" + $(Join-Path $PsScriptRoot 'helpers/PsFzfGitBranches-Preview.sh') + "\"" {}" $result = @() # use pwsh to prevent bash from trying to write to host output: $branches = & $script:pwshExec -NoProfile -NonInteractive -Command "& ${script:bashPath} '$(Join-Path $PsScriptRoot 'helpers/PsFzfGitBranches.sh')' branches" $branches | Invoke-Fzf @fzfArguments -Preview "$previewCmd" -Prompt '🌲 Branches> ' -HeaderLines 2 -Tiebreak begin -ReverseInput | ` ForEach-Object { $result += $($_.Substring('* '.Length) -split ' ')[0] } $result } function Invoke-PsFzfGitTags() { if (-not (IsInGitRepo)) { return } if (-not $(SetupGitPaths)) { Write-Error "git executable could not be found" return } $fzfArguments = Get-GitFzfArguments $fzfArguments['PreviewWindow'] = 'right,70%' $previewCmd = "git show --color=always {}" $result = @() git tag --sort -version:refname | Invoke-Fzf @fzfArguments -Preview "$previewCmd" -Prompt '📛 Tags> ' | ` ForEach-Object { $result += $_ } $result } function Invoke-PsFzfGitStashes() { if (-not (IsInGitRepo)) { return } if (-not $(SetupGitPaths)) { Write-Error "git executable could not be found" return } $fzfArguments = Get-GitFzfArguments $fzfArguments['Bind'] += 'ctrl-x:execute-silent(git stash drop {1})+reload(git stash list)' $header = "CTRL-X (drop stash)`n`n" $previewCmd = 'git show --color=always {1}' $result = @() git stash list --color=always | Invoke-Fzf @fzfArguments -Header $header -Delimiter ':' -Preview "$previewCmd" -Prompt '🥡 Stashes> ' | ` ForEach-Object { $result += $_.Split(':')[0] } $result } # PSFzf.TabExpansion.ps1 # borrowed from https://github.com/dahlbyk/posh-git/blob/f69efd9229029519adb32e37a464b7e1533a372c/src/GitTabExpansion.ps1#L81 filter script:quoteStringWithSpecialChars { if ($_ -and ($_ -match '\s+|#|@|\$|;|,|''|\{|\}|\(|\)')) { $str = $_ -replace "'", "''" "'$str'" } else { $_ } } # taken from https://github.com/dahlbyk/posh-git/blob/2ad946347e7342199fd4bb1b42738833f68721cd/src/GitUtils.ps1#L407 function script:Get-AliasPattern($cmd) { $aliases = @($cmd) + @(Get-Alias | Where-Object { $_.Definition -eq $cmd } | Select-Object -Exp Name) "($($aliases -join '|'))" } function Expand-GitWithFzf($lastBlock) { $gitResults = Expand-GitCommand $lastBlock # if no results, invoke filesystem completion: if ($null -eq $gitResults) { $results = Invoke-Fzf -Multi | script:quoteStringWithSpecialChars } else { $results = $gitResults | Invoke-Fzf -Multi | script:quoteStringWithSpecialChars } if ($results.Count -gt 1) { $results -join ' ' } else { if (-not $null -eq $results) { $results } else { '' # output something to prevent default tab expansion } } InvokePromptHack } function Expand-FileDirectoryPath($lastWord) { # find dir and file pattern connected to the trigger: $lastWord = $lastWord.Substring(0, $lastWord.Length - 2) if ($lastWord.EndsWith('\')) { $dir = $lastWord.Substring(0, $lastWord.Length - 1) $file = $null } elseif (-not [string]::IsNullOrWhiteSpace($lastWord)) { $dir = Split-Path $lastWord -Parent $file = Split-Path $lastWord -Leaf } if (-not [System.IO.Path]::IsPathRooted($dir)) { $dir = Join-Path $PWD.ProviderPath $dir } $prevPath = $Pwd.ProviderPath try { if (-not [string]::IsNullOrEmpty($dir)) { Set-Location $dir } if (-not [string]::IsNullOrEmpty($file)) { Invoke-Fzf -Query $file } else { Invoke-Fzf } } finally { Set-Location $prevPath } InvokePromptHack } $script:TabExpansionEnabled = $false function SetTabExpansion($enable) { if ($enable) { if (-not $script:TabExpansionEnabled) { $script:TabExpansionEnabled = $true RegisterBuiltinCompleters Register-ArgumentCompleter -CommandName git, tgit, gitk -Native -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) # The PowerShell completion has a habit of stripping the trailing space when completing: # git checkout <tab> # The Expand-GitCommand expects this trailing space, so pad with a space if necessary. $padLength = $cursorPosition - $commandAst.Extent.StartOffset $textToComplete = $commandAst.ToString().PadRight($padLength, ' ').Substring(0, $padLength) Expand-GitCommandPsFzf $textToComplete } } } else { if ($script:TabExpansionEnabled) { $script:TabExpansionEnabled = $false } } } function CheckFzfTrigger { param($commandName, $parameterName, $wordToComplete, $commandAst, $cursorPosition, $action) if ([string]::IsNullOrWhiteSpace($env:FZF_COMPLETION_TRIGGER)) { $completionTrigger = '**' } else { $completionTrigger = $env:FZF_COMPLETION_TRIGGER } if ($wordToComplete.EndsWith($completionTrigger)) { $wordToComplete = $wordToComplete.Substring(0, $wordToComplete.Length - $completionTrigger.Length) $wordToComplete } } function GetServiceSelection() { param( [scriptblock] $ResultAction ) $header = [System.Environment]::NewLine + $("{0,-24} NAME" -f "DISPLAYNAME") + [System.Environment]::NewLine $result = Get-Service | Where-Object { ![string]::IsNullOrEmpty($_.Name) } | ForEach-Object { "{0,-24} {1}" -f $_.DisplayName.Substring(0, [System.Math]::Min(24, $_.DisplayName.Length)), $_.Name } | Invoke-Fzf -Multi -Header $header $result | ForEach-Object { &$ResultAction $_ } } function RegisterBuiltinCompleters { $processIdOrNameScriptBlock = { param($commandName, $parameterName, $wordToComplete, $commandAst, $cursorPosition, $action) $wordToComplete = CheckFzfTrigger $commandName $parameterName $wordToComplete $commandAst $cursorPosition if ($null -ne $wordToComplete) { $selectType = $parameterName $script:resultArr = @() GetProcessSelection -ResultAction { param($result) $resultSplit = $result.split(' ', [System.StringSplitOptions]::RemoveEmptyEntries) if ($selectType -eq 'Name') { $processNameIdx = 3 $script:resultArr += $resultSplit[$processNameIdx..$resultSplit.Length] -join ' ' } elseif ($selectType -eq 'Id') { $processIdIdx = 2 $script:resultArr += $resultSplit[$processIdIdx] } } if ($script:resultArr.Length -ge 1) { $script:resultArr -join ', ' } InvokePromptHack } else { # don't return anything - let normal tab completion work } } 'Get-Process', 'Stop-Process' | ForEach-Object { Register-ArgumentCompleter -CommandName $_ -ParameterName "Name" -ScriptBlock $processIdOrNameScriptBlock Register-ArgumentCompleter -CommandName $_ -ParameterName "Id" -ScriptBlock $processIdOrNameScriptBlock } $serviceNameScriptBlock = { param($commandName, $parameterName, $wordToComplete, $commandAst, $cursorPosition, $action) $wordToComplete = CheckFzfTrigger $commandName $parameterName $wordToComplete $commandAst $cursorPosition if ($null -ne $wordToComplete) { if ($parameterName -eq 'Name') { $group = '$2' } elseif ($parameterName -eq 'DisplayName') { $group = '$1' } $script:resultArr = @() GetServiceSelection -ResultAction { param($result) $script:resultArr += $result.Substring(24 + 1) } if ($script:resultArr.Length -ge 1) { $script:resultArr -join ', ' } InvokePromptHack } else { # don't return anything - let normal tab completion work } } 'Get-Service', 'Start-Service', 'Stop-Service' | ForEach-Object { Register-ArgumentCompleter -CommandName $_ -ParameterName "Name" -ScriptBlock $serviceNameScriptBlock Register-ArgumentCompleter -CommandName $_ -ParameterName "DisplayName" -ScriptBlock $serviceNameScriptBlock } } function Expand-GitCommandPsFzf($lastWord) { if ([string]::IsNullOrWhiteSpace($env:FZF_COMPLETION_TRIGGER)) { $completionTrigger = '**' } else { $completionTrigger = $env:FZF_COMPLETION_TRIGGER } if ($lastWord.EndsWith($completionTrigger)) { $lastWord = $lastWord.Substring(0, $lastWord.Length - $completionTrigger.Length) Expand-GitWithFzf $lastWord } else { Expand-GitCommand $lastWord } } function Invoke-FzfTabCompletion() { $script:continueCompletion = $true do { $script:continueCompletion = script:Invoke-FzfTabCompletionInner } while ($script:continueCompletion) } function script:Invoke-FzfTabCompletionInner() { $script:result = @() [void] [System.Reflection.Assembly]::LoadWithPartialName("System.Management.Automation") $line = $null $cursor = $null [Microsoft.PowerShell.PSConsoleReadline]::GetBufferState([ref]$line, [ref]$cursor) if ($cursor -lt 0 -or [string]::IsNullOrWhiteSpace($line)) { return $false } try { $completions = [System.Management.Automation.CommandCompletion]::CompleteInput($line, $cursor, @{}) } catch { # some custom tab completions will cause CompleteInput() to throw, so we gracefully handle those cases. # For example, see the issue https://github.com/kelleyma49/PSFzf/issues/95. return $false } $completionMatches = $completions.CompletionMatches if ($completionMatches.Count -le 0) { return $false } $script:continueCompletion = $false if ($completionMatches.Count -eq 1) { $script:result = $completionMatches[0].CompletionText } elseif ($completionMatches.Count -gt 1) { $script:result = @() $script:checkCompletion = $true $cancelTrigger = 'ESC' $expectTriggers = "${script:TabContinuousTrigger},${cancelTrigger}" # normalize so path works correctly for Windows: $path = $PWD.ProviderPath.Replace('\', '/') # need to handle parameters differently so PowerShell doesn't parse completion item as a script parameter: if ( $completionMatches[0].ResultType -eq 'ParameterName') { $Command = $Line.Substring(0, $Line.indexof(' ')) $previewScript = $(Join-Path $PsScriptRoot 'helpers/PsFzfTabExpansion-Parameter.ps1') $additionalCmd = @{ Preview = $("$PowerShellCMD -NoProfile -NonInteractive -File \""$previewScript\"" $Command {}") } } else { $previewScript = $(Join-Path $PsScriptRoot 'helpers/PsFzfTabExpansion-Preview.ps1') $additionalCmd = @{ Preview = $($script:PowershellCmd + " -NoProfile -NonInteractive -File \""$previewScript\"" \""" + $path + "\"" {}") } } $script:fzfOutput = @() $completionMatches | ForEach-Object { $_.CompletionText } | Invoke-Fzf ` -Layout reverse ` -Expect "$expectTriggers" ` -PreviewWindow 'down:30%' ` -Bind 'tab:down', 'btab:up', 'ctrl-/:change-preview-window(down,right:50%,border-top|hidden|)' ` @additionalCmd | ForEach-Object { $script:fzfOutput += $_ } if ($script:fzfOutput[0] -eq $cancelTrigger) { InvokePromptHack return $false } elseif ($script:fzfOutput.Length -gt 1) { $script:result = $script:fzfOutput[1] } # check if we should continue completion: $script:continueCompletion = $script:fzfOutput[0] -eq $script:TabContinuousTrigger InvokePromptHack } $result = $script:result if ($null -ne $result) { # quote strings if we need to: if ($result -is [system.array]) { for ($i = 0; $i -lt $result.Length; $i++) { $result[$i] = FixCompletionResult $result[$i] } $str = $result -join ',' } else { $str = FixCompletionResult $result } if ($script:continueCompletion) { $isQuoted = $str.EndsWith("'") $resultTrimmed = $str.Trim(@('''', '"')) if (Test-Path "$resultTrimmed" -PathType Container) { if ($isQuoted) { $str = "'{0}{1}'" -f "$resultTrimmed", $script:TabContinuousTrigger } else { $str = "$resultTrimmed" + $script:TabContinuousTrigger } } else { # no more paths to complete, so let's stop completion: $str += ' ' $script:continueCompletion = $false } } $leftCursor = $completions.ReplacementIndex $replacementLength = $completions.ReplacementLength if ($leftCursor -le 0 -and $replacementLength -le 0) { [Microsoft.PowerShell.PSConsoleReadLine]::Insert($str) } else { [Microsoft.PowerShell.PSConsoleReadLine]::Replace($leftCursor, $replacementLength, $str) } } return $script:continueCompletion } |