Complete/CompleteFile.ps1

# Copyright (C) 2024 kzrnm
# Based on git-completion.bash (https://github.com/git/git/blob/HEAD/contrib/completion/git-completion.bash).
# Distributed under the GNU General Public License, version 2.0.
using namespace System.Collections.Generic;
using namespace System.Management.Automation;
using namespace System.IO;

function escapeSpecialChar {
    param (
        [Parameter(Position = 0, ValueFromPipeline)]
        [string]
        $Text
    )

    process {
        $Text -creplace '(["''`\s])', '`$1'
    }
}

function completeLocalFile {
    param (
        [Parameter(ValueFromPipeline)]
        [string]
        $File,
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$Current,
        [string]$Prefix = '',
        [string]$BaseDir = '',
        [switch]
        $RemovePrefix
    )

    process {
        if ($RemovePrefix -and $Current.StartsWith($Prefix)) {
            $Current = $Current.Substring($Prefix.Length)
        }

        if ((!$Current) -or $File.StartsWith($Current)) {
            if ($BaseDir -and !$BaseDir.EndsWith('/')) {
                $BaseDir += '/'
            }
            $fullPath = (Resolve-Path "${BaseDir}${File}").Path

            $ListItem = "$File$Suffix"
            $Completion = "$Prefix$File$Suffix"

            [CompletionResult]::new(
                "$Completion",
                "$ListItem",
                'ProviderItem',
                $fullPath.TrimEnd([Path]::AltDirectorySeparatorChar, [Path]::DirectorySeparatorChar)
            )
        }
    }
}

function completeCurrentDirectory {
    [CmdletBinding()]
    [OutputType([CompletionResult[]])]
    param (
        [Parameter(Mandatory, Position = 0)]
        [AllowEmptyString()]
        [string]
        $Current,
        [string]
        $Prefix = ''
    )

    $lx = $Current.LastIndexOfAny(@([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar))
    $left = $Current.Substring(0, $lx + 1) | escapeSpecialChar

    Get-ChildItem "$Current*" | ForEach-Object {
        $name = $_.Name | escapeSpecialChar
        if ($_ -is [System.IO.DirectoryInfo]) {
            [CompletionResult]::new(
                "${Prefix}${left}${name}/",
                $_.Name + '/',
                'ProviderItem',
                $_.FullName
            )
        }
        else {
            [CompletionResult]::new(
                "${Prefix}${left}${name}",
                $_.Name,
                'ProviderItem',
                $_.FullName
            )
        }
    }
    return
}

function filterFiles {
    [CmdletBinding()]
    [OutputType([CompletionResult[]])]
    param (
        [Parameter(ValueFromPipeline)]
        [AllowEmptyCollection()]
        [string]
        $Candidate,
        [string[]]
        $Exclude = @()
    )
    begin {
        $ex = [HashSet[string]]::new($Exclude.Length)
        foreach ($e in $Exclude) {
            $ex.Add($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($e)) | Out-Null
        }
    }

    process {
        if (!$ex.Contains($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Candidate))) {
            $Candidate
        }
    }
}

function completeFromFileList {
    [CmdletBinding()]
    [OutputType([CompletionResult[]])]
    param (
        [Parameter(ValueFromPipeline)]
        [AllowEmptyCollection()]
        [string]
        $Candidate,
        [string]
        $Prefix = '',
        [switch]
        $LeadingDash
    )
    begin {
        [string]$Prev = ''
    }

    process {
        if (!$Candidate) {}
        elseif (!$Prev) {
            $Prev = $Candidate
        }
        else {
            $CommonPrefixLength = $Prefix.Length
            for ($i = $Prefix.Length; $i -lt $Prev.Length; $i++) {
                if ($Candidate[$i] -cne $Prev[$i]) {
                    if ($CommonPrefixLength -gt $Prefix.Length) {
                        $Prev = $Prev.Substring(0, $CommonPrefixLength)
                    }
                    else {
                        $Completion = $Prev
                        if (!$LeadingDash -and ($Prev.StartsWith('-'))) {
                            $Completion = "./$Completion"
                        }
                        [CompletionResult]::new(
                            ($Completion | escapeSpecialChar),
                            $Prev,
                            'ProviderItem',
                            $Prev
                        )
                        $Prev = $Candidate
                    }
                    break
                }
                elseif ($Candidate[$i] -cin @([Path]::DirectorySeparatorChar, [Path]::AltDirectorySeparatorChar)) {
                    $CommonPrefixLength = $i + 1
                }
            }
        }
    }

    end {
        if ($Prev) {
            $Completion = $Prev
            if (!$LeadingDash -and ($Prev.StartsWith('-'))) {
                $Completion = "./$Completion"
            }
            [CompletionResult]::new(
                ($Completion | escapeSpecialChar),
                $Prev,
                'ProviderItem',
                $Prev
            )
        }
    }
}