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,
            [ValidateSet('default','path','history')]
              [string]
              $Scheme = $null,
              [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', 'bold', 'block', 'double', 'horizontal', 'vertical', 'top', 'bottom', 'left', 'right', 'none')]
            [string]$BorderStyle,
            [string]$BorderLabel,
            [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('Scheme') -and ![string]::IsNullOrWhiteSpace($Scheme))                 { $arguments += "--scheme=$Scheme "}
        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('BorderLabel') -and ![string]::IsNullOrWhiteSpace($BorderLabel))        { $arguments += "--border-label=""$BorderLabel"" "}
        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
    }
    $isUsingPath = -not [string]::IsNullOrWhiteSpace($currentPath)

    $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 (-not $isUsingPath) {
                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++) {
                if ($isUsingPath) {
                    $resultFull = Join-Path $currentPath $result[$i]
                }
                else {
                    $resultFull = $result[$i]
                }
                $result[$i] = FixCompletionResult $resultFull
            }
        }
        else {
            if ($isUsingPath) {
                $result = Join-Path $currentPath $result
            }
            $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 Get-PickedHistory($Query = '', [switch]$UsePSReadLineHistory) {
    try {
        $script:OverrideFzfDefaults = [FzfDefaultOpts]::new($env:FZF_CTRL_R_OPTS)

        $fileHist = @{}
        if ($UsePSReadLineHistory) {
            $reader = New-Object PSFzf.IO.ReverseLineReader -ArgumentList $((Get-PSReadlineOption).HistorySavePath)

            $result = $reader.GetEnumerator() | ForEach-Object {
                if (-not $fileHist.ContainsKey($_)) {
                    $fileHist.Add($_, $true)
                    $_
                }
            } | Invoke-Fzf -Query "$Query" -Bind ctrl-r:toggle-sort, ctrl-z:ignore -Scheme history
        }
        else {
            $result = Get-History | ForEach-Object { $_.CommandLine } | ForEach-Object {
                if (-not $fileHist.ContainsKey($_)) {
                    $fileHist.Add($_, $true)
                    $_
                }
            } | Invoke-Fzf -Query "$Query" -Reverse -Scheme history
        }

    }
    catch {
        # catch custom exception
    }
    finally {
        if ($script:OverrideFzfDefaults) {
            $script:OverrideFzfDefaults.Restore()
            $script:OverrideFzfDefaults = $null
        }

        # ensure that stream is closed:
        if ($reader) {
            $reader.Dispose()
        }
    }

    $result
}
function Invoke-FzfPsReadlineHandlerHistory {
    $result = $null
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadline]::GetBufferState([ref]$line, [ref]$cursor)

    $result = Get-PickedHistory -Query $line -UsePSReadLineHistory

    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 -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
        if ($(Get-PSReadLineOption).EditMode -eq [Microsoft.PowerShell.EditMode]::Vi) {
            Set-PSReadlineKeyHandler -Key $Chord -ViMode Command -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 Ignore
            $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' -or $editor -eq 'code-insiders' -or $editor -eq 'codium') {
        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
        }
    }
    else {
        # select the first file as we don't know if the editor supports opening multiple files from the cmd line
        if ($FileList -is [array] -and $FileList.length -gt 1) {
            "$editor$editorOptions ""{0}""" -f $(Resolve-Path $FileList[0].Trim('"'))
        }
        else {
            "$editor$editorOptions ""{0}""" -f $(Resolve-Path $FileList.Trim('"'))
        }
    }
}
function Invoke-FuzzyEdit() {
    param($Directory = ".", [switch]$Wait)

    $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'..."
            ($Editor, $Arguments) = $cmd.Split(' ')
            Start-Process $Editor -ArgumentList $Arguments -Wait:$Wait -NoNewWindow
        }
        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() {
    $result = Get-PickedHistory -UsePSReadLineHistory:$($null -ne $(Get-Command Get-PSReadLineOption -ErrorAction Ignore))
    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 {
        Get-ChildItem $Directory -Recurse -ErrorAction Ignore | Where-Object { $_.PSIsContainer } | 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 Ignore)
            $script:bashPath = 'bash'
            $script:grepPath = 'grep'
        }
        else {
            $gitInfo = Get-Command git.exe -ErrorAction Ignore
            $script:foundGit = $null -ne $gitInfo
            if ($script:foundGit) {
                # Detect if scoop is installed
                $script:scoopInfo = Get-Command scoop -ErrorAction Ignore
                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 Ignore) {
            @('ctrl+g,ctrl+b', 'Select Git branches via fzf', { Update-CmdLine $(Invoke-PsFzfGitBranches) }), `
            @('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+p', 'Select Git pull requests via fzf', { Update-CmdLine $(Invoke-PsFzfGitPulLRequests) }), `
            @('ctrl+g,ctrl+s', 'Select Git stashes via fzf', { Update-CmdLine $(Invoke-PsFzfGitStashes) }), `
            @('ctrl+g,ctrl+t', 'Select Git tags via fzf', { Update-CmdLine $(Invoke-PsFzfGitTags) }) `
            | 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 `
        -BorderLabel '📁 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  `
        -BorderLabel '🍡 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%'
    $gitBranchesHelperPath = Join-Path $PsScriptRoot 'helpers/PsFzfGitBranches.sh'
    $ShortcutBranchesAll = "ctrl-a:change-prompt(🌳 All branches> )+reload(" + """${script:bashPath}"" '${gitBranchesHelperPath}' all-branches)"
    $fzfArguments['Bind'] += 'ctrl-/:change-preview-window(down,70%|hidden|)', $ShortcutBranchesAll

    $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} '$gitBranchesHelperPath' branches"
    $branches |
    Invoke-Fzf @fzfArguments -Preview "$previewCmd" -BorderLabel '🌲 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" -BorderLabel '📛 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" -BorderLabel '🥡 Stashes' | `
        ForEach-Object {
        $result += $_.Split(':')[0]
    }

    $result
}

function Invoke-PsFzfGitPullRequests() {
    if (-not (IsInGitRepo)) {
        return
    }

    if (-not $(SetupGitPaths)) {
        Write-Error "git executable could not be found"
        return
    }
    # find the repo remote URL
    $remoteUrl = git config --get remote.origin.url

    # GitHub
    if ($remoteUrl -match 'github.com') {
        $script:ghCmdInfo = Get-Command gh -ErrorAction Ignore
        if ($null -ne $script:ghCmdInfo) {
            $listAllPrsCmdJson = Invoke-Expression "gh pr list --json id,author,title,number"
            $objs = $listAllPrsCmdJson | ConvertFrom-Json | ForEach-Object {
                [PSCustomObject]@{
                    PR      = "$($PSStyle.Foreground.Green)" + $_.number
                    Title   = "$($PSStyle.Foreground.Magenta)" + $_.title
                    Creator = "$($PSStyle.Foreground.Yellow)" + $_.author.login
                }
            }
        }
        else {
            Write-Error "Repo is a GitHub repo and gh command not found"
            return
        }
        $webCmd = 'gh pr view {1} --web'
        $previewCmd = 'gh pr view {1} && gh pr diff {1}'
    }
    # Azure DevOps
    elseif ($remoteUrl -match 'dev.azure.com|visualstudio.com') {
        $script:azCmdInfo = Get-Command az -ErrorAction Ignore
        if ($null -ne $script:azCmdInfo) {
            $listAllPrsCmdJson = Invoke-Expression 'az repos pr list --status "active" --query "[].{title: title, number: pullRequestId, creator: createdBy.uniqueName}"'
            $objs = $listAllPrsCmdJson | ConvertFrom-Json | ForEach-Object {
                [PSCustomObject]@{
                    PR      = "$($PSStyle.Foreground.Green)" + $_.number
                    Title   = "$($PSStyle.Foreground.Magenta)" + $_.title
                    Creator = "$($PSStyle.Foreground.Yellow)" + $_.creator
                }
            }
        }
        else {
            Write-Error "Repo is an Azure DevOps repo and az command not found"
            return
        }
        $webCmd = 'az repos pr show --id {1} --open --output none'
        # currently errors on query. Need to fix instead of output everything
        #$previewCmd = 'az repos pr show --id {1} --query "{Created:creationDate, Closed:closedDate, Creator:createdBy.displayName, PR:codeReviewId, Title:title, Repo:repository.name, Reviewers:join('', '',reviewers[].displayName), Source:sourceRefName, Target:targetRefName}" --output yamlc'
        $previewCmd = 'az repos pr show --id {1} --output yamlc'
    }

    $fzfArguments = Get-GitFzfArguments
    $fzfArguments['Bind'] += 'ctrl-o:execute-silent(' + $webCmd + ')'
    $header = "CTRL-O (open in browser)`n`n"

    $prevCLICOLOR_FORCE = $env:CLICOLOR_FORCE
    $prevOutputRendering = $PSStyle.OutputRendering

    $env:CLICOLOR_FORCE = 1 # make gh show keep colors
    $PSStyle.OutputRendering = 'Ansi'

    try {
        $result = @()
        $objs | out-string -Stream  | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | `
            Invoke-Fzf @fzfArguments -Header $header -Preview "$previewCmd" -HeaderLines 2 -BorderLabel '🆕 Pull Requests' | `
            ForEach-Object {
            $result += $_.Split(' ')[0] # get the PR ID
        }
    }
    finally {
        $env:CLICOLOR_FORCE = $prevCLICOLOR_FORCE
        $PSStyle.OutputRendering = $prevOutputRendering
    }

    $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

            # borrowed from https://github.com/dahlbyk/posh-git/blob/70e44dc0c2cdaf10c0cc8eb9ef5a9ca65ab63dcf/src/GitTabExpansion.ps1#L544C58-L544C58
            $cmdNames = "git", "tgit", "gitk"
            # Create regex pattern from $cmdNames: ^(git|git\.exe|tgit|tgit\.exe|gitk|gitk\.exe)$
            $cmdNamesPattern = "^($($cmdNames -join '|'))(\.exe)?$"
            $cmdNames += Get-Alias | Where-Object { $_.Definition -match $cmdNamesPattern } | Foreach-Object Name

            Register-ArgumentCompleter -CommandName $cmdNames -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
}