CompletionRoot.ps1

. $PSScriptRoot/GitCommands.ps1
. $PSScriptRoot/GitStatics.ps1
. $PSScriptRoot/CompletionUtil.ps1
. $PSScriptRoot/CompleteConfig.ps1
. $PSScriptRoot/CompleteSubCommands.ps1

function WriteLog {
    param([Parameter(Position = 0)]$Object)
    
    if ($env:GitCompletionDubugPath) {
        $Object >> $env:GitCompletionDubugPath
    }
}

# __git_main
function Complete-Git-Ast {
    [OutputType([System.Management.Automation.CompletionResult[]])]
    param(
        [System.Management.Automation.Language.CommandAst]
        $CommandAst,
        [int]
        $CursorPosition
    )

    if ($env:GitCompletionDubugPath -and (-not $env:GitCompletionDubugKeep)) {
        $null > $env:GitCompletionDubugPath
    }

    $CommandAst = $CommandAst
    $script:CursorPosition = $CursorPosition
    $script:Words = ($CommandAst.CommandElements | Select-Object -ExpandProperty Extent | Select-Object -ExpandProperty Text)

    $cw = $CommandAst.CommandElements.Count
    $pr = $script:Words[$script:Words.Count - 1]
    $cr = ''

    for ($i = 1; $i -lt $CommandAst.CommandElements.Count; $i++) {
        $extent = $CommandAst.CommandElements[$i].Extent

        if ($CursorPosition -lt $extent.StartOffset) {
            # The cursor is within whitespace between the previous and current words.
            $pr = $commandAst.CommandElements[$i - 1].Extent.Text
            $cw = $i
            break
        }
        elseif ($CursorPosition -le $extent.EndOffset) {
            $cr = $extent.Text
            $pr = $commandAst.CommandElements[$i - 1].Extent.Text
            $cw = $i
            break
        }
    }

    $script:WordPosition = $cw
    $script:CurrentWord = $cr
    $script:PreviousWord = $pr

    WriteLog "CommandAst=$CommandAst"
    Complete-Git `
        -CursorPosition $CursorPosition `
        -Words $Words `
        -WordPosition $WordPosition `
        -CurrentWord $CurrentWord `
        -PreviousWord $PreviousWord
}

$gitGlobalOptions = @(
    (
        New-CommandOption -Short '-v' -Long '--version' `
            -Desc 'Prints the Git suite version.'
    ),
    (
        New-CommandOption -Short '-h' -Long '--help' `
            -Desc 'Prints the helps. If --all is given then all available commands are printed.'
    ),
    (
        New-CommandOption -Short '-C' `
            -Value '<path>' `
            -Desc 'Run as if git was started in <path> instead of the current working directory.' 
    ),
    (
        New-CommandOption -Short '-c' `
            -Value '<name>=<value>' `
            -Desc 'Pass a configuration parameter to the command.'
    ),
    (
        New-CommandOption -Long '--config-env' `
            -Value '<name>=<envvar>' `
            -Desc 'Like -c <name>=<value>, give configuration variable <name> a value, where <envvar> is the name of an environment variable from which to retrieve the value.'
    ),
    (
        New-CommandOption -Long '--exec-path' `
            -Value '<path>' `
            -Desc 'Path to wherever your core Git programs are installed.'
    ),
    (
        New-CommandOption -Long '--html-path' `
            -Desc "Print the path, without trailing slash, where Git’s HTML documentation is installed and exit."
    ),
    (
        New-CommandOption -Long '--man-path' `
            -Desc 'Print the manpath for the man pages for this version of Git and exit.'
    ),
    (
        New-CommandOption -Long '--info-path' `
            -Desc 'Print the path where the Info files documenting this version of Git are installed and exit.'
    ),
    (
        New-CommandOption -Short '-p' -Long '--paginate' `
            -Desc 'Pipe all output into less (or if set, $PAGER) if standard output is a terminal.'
    ),
    (
        New-CommandOption -Short '-P' -Long '--no-pager' `
            -Desc 'Do not pipe Git output into a pager.'
    ),
    (
        New-CommandOption -Long '--git-dir' `
            -Desc 'Set the path to the repository (".git" directory).'
    ),
    (
        New-CommandOption -Long '--work-tree' `
            -Value '<path>' `
            -Desc 'Set the path to the working tree.'
    ),
    (
        New-CommandOption -Long '--namespace' `
            -Value '<path>' `
            -Desc 'Set the Git namespace.'
    ),
    (
        New-CommandOption -Long '--bare' `
            -Desc 'Treat the repository as a bare repository.'
    ),
    (
        New-CommandOption -Long '--no-replace-objects' `
            -Desc 'Do not use replacement refs to replace Git objects.'
    ),
    (
        New-CommandOption -Long '--no-lazy-fetch' `
            -Desc 'Do not fetch missing objects from the promisor remote on demand.'
    ),
    (
        New-CommandOption -Long '--literal-pathspecs' `
            -Desc 'Treat pathspecs literally (i.e. no globbing, no pathspec magic).'
    ),
    (
        New-CommandOption -Long '--glob-pathspecs' `
            -Desc 'Add "glob" magic to all pathspec.'
    ),
    (
        New-CommandOption -Long '--noglob-pathspecs' `
            -Desc 'Add "literal" magic to all pathspec.'
    ),
    (
        New-CommandOption -Long '--icase-pathspecs' `
            -Desc 'Add "icase" magic to all pathspec.'
    ),
    (
        New-CommandOption -Long '--no-optional-locks' `
            -Desc 'Do not perform optional operations that require locks.'
    ),
    (
        New-CommandOption -Long '--list-cmds' `
            -Value '<group>[,<group>…​]' `
            -Desc 'List commands by group.'
    ),
    (
        New-CommandOption -Long '--no-replace-objects' `
            -Desc 'List commands by group.'
    ),
    (
        New-CommandOption  -Long '--attr-source' `
            -Desc 'Read gitattributes from <tree-ish> instead of the worktree.' `
            -Value '<tree-ish>'
    )
)

$gitSubCommandDescriptions = @{
    'clone'   = 'Clone a repository into a new directory';
    'init'    = 'Create an empty Git repository or reinitialize an existing one';
    'add'     = 'Add file contents to the index';
    'mv'      = 'Move or rename a file, a directory, or a symlink';
    'restore' = 'Restore working tree files';
    'rm'      = 'Remove files from the working tree and from the index';
    'bisect'  = 'Use binary search to find the commit that introduced a bug';
    'diff'    = 'Show changes between commits, commit and working tree, etc';
    'grep'    = 'Print lines matching a pattern';
    'log'     = 'Show commit logs';
    'show'    = 'Show various types of objects';
    'status'  = 'Show the working tree status';
    'branch'  = 'List, create, or delete branches';
    'commit'  = 'Record changes to the repository';
    'merge'   = 'Join two or more development histories together';
    'rebase'  = 'Reapply commits on top of another base tip';
    'reset'   = 'Reset current HEAD to the specified state';
    'switch'  = 'Switch branches';
    'tag'     = 'Create, list, delete or verify a tag object signed with GPG';
    'fetch'   = 'Download objects and refs from another repository';
    'pull'    = 'Fetch from and integrate with another repository or a local branch';
    'push'    = 'Update remote refs along with associated objects';
}

function Complete-Git {
    [CmdletBinding(PositionalBinding = $false)]
    [OutputType([System.Management.Automation.CompletionResult[]])]
    param(
        [int][Parameter(Mandatory)]$CursorPosition,
        [string[]][AllowEmptyCollection()][AllowEmptyString()][Parameter(Mandatory)]$Words,
        [int][Parameter(Mandatory)]$WordPosition,
        [string][AllowEmptyString()][Parameter(Mandatory)]$CurrentWord,
        [string][AllowEmptyString()][Parameter(Mandatory)]$PreviousWord
    )
    
    WriteLog "CursorPosition=$CursorPosition"
    WriteLog "Words=$Words"
    WriteLog "WordPosition=$WordPosition"
    WriteLog "CurrentWord=$CurrentWord"
    WriteLog "PreviousWord=$PreviousWord"

    $script:CursorPosition = $CursorPosition
    $script:Words = $Words
    $script:gitCArgs = $gitCArgs
    $script:command = $command
    $script:commandIndex = $commandIndex
    $script:WordPosition = $WordPosition
    $script:CurrentWord = $CurrentWord
    $script:PreviousWord = $PreviousWord
    
    $script:__git_repo_path = $null
    $script:gitDir = $null
    $script:gitCArgs = @()
    $script:command = ''
    $script:commandIndex = 0

    :globalflag for ($i = 1; $i -lt $script:WordPosition; $i++) {
        $s = $script:Words[$i]
        switch -Wildcard -CaseSensitive ($s) {
            '--git-dir=*' {
                $script:gitDir = [System.IO.DirectoryInfo]::new($s.Substring('--git-dir='.Length))
                continue
            }
            '--git-dir' {
                if (++$i -lt $script:Words.Count) {
                    $script:gitDir = [System.IO.DirectoryInfo]::new($script:Words[$i])
                }
                continue
            }
            '--bare' {
                $script:gitDir = [System.IO.DirectoryInfo]::new('.')
                continue
            }
            '--help' {
                $script:command = 'help'
                break globalflag
            }
            { $_ -cin @('-c', '--work-tree', '--namespace') } {
                ++$i
                continue
            }
            '-C' {
                $script:gitCArgs += @('-C', $script:Words[++$i])
                continue
            }
            '-*' {
                continue
            }
            default {
                $script:command = $s
                $script:commandIndex = $i
                break globalflag
            }
        }
    }
    WriteLog "gitDir=$gitDir"
    WriteLog "gitCArgs=$gitCArgs"
    WriteLog "command=$command"
    WriteLog "commandIndex=$commandIndex"

    if ($command) {
        return CompleteSubCommands
    }

    switch -Wildcard -CaseSensitive ($PreviousWord) {
        { $_ -cin @('-C', '--work-tree', '--git-dir') } {
            # these need a path argument
            return
        }
        '-c' {
            completeConfigVariableNameAndValue -Current $CurrentWord
            return
        }
        '--namespace' {
            # we don't support completing these options' arguments
            return
        }
    }

    switch -Wildcard ($CurrentWord) {
        '--*' {
            $gitGlobalOptions | ForEach-Object { $_.ToLongCompletion($CurrentWord) } | Where-Object { $_ }
            return
        }
        '-*' {
            if ($CurrentWord -eq '-') {
                $gitGlobalOptions | ForEach-Object { $_.ToShortCompletion() } | Where-Object { $_ }
            }
            return
        }
    }

    $descriptions = $gitSubCommandDescriptions.Clone()
    foreach ($a in (gitListAliases)) {
        $descriptions[$a.Name] = "[alias] $($a.Value)"
    }

    $commands = (gitAllCommands builtins list-mainporcelain others nohelpers alias list-complete config)
    $commands -clike "$script:CurrentWord*" | Sort-Object -Unique | ForEach-Object {
        $desc = $descriptions[$_]
        if (-not $desc) {
            $desc = $_
        }
        [System.Management.Automation.CompletionResult]::new(
            "$_",
            "$_",
            "Text",
            $desc
        )
    }
}