core/powershell/completion.ps1

Add-Member -InputObject $PSCompletions -MemberType ScriptMethod handle_completion {
    param([string]$cmd)
    $this.cmd[$cmd] | ForEach-Object {
        Register-ArgumentCompleter -CommandName $_ -ScriptBlock {
            param($word_to_complete, $command_ast, $cursor_position)
            function ConvertFrom-JsonToHashtable([string]$json) {
                # Handle json string
                $matches = [regex]::Matches($json, '\s*"\s*"\s*:')
                foreach ($match in $matches) {
                    $json = $json -replace $match.Value, "`"empty_key_$([System.Guid]::NewGuid().Guid)`":"
                }
                $json = [regex]::Replace($json, ",`n?(\s*`n)?\}", "}")
                function ConvertToHashtable($obj) {
                    $hash = @{}
                    if ($obj -is [System.Management.Automation.PSCustomObject]) {
                        $obj | Get-Member -MemberType Properties | ForEach-Object {
                            $k = $_.Name # Key
                            $v = $obj.$k # Value
                            if ($v -is [System.Collections.IEnumerable] -and $v -isnot [string]) {
                                # Handle array
                                $arr = @()
                                foreach ($item in $v) {
                                    $arr += if ($item -is [System.Management.Automation.PSCustomObject]) { ConvertToHashtable($item) }else { $item }
                                }
                                $hash[$k] = $arr
                            }
                            elseif ($v -is [System.Management.Automation.PSCustomObject]) {
                                # Handle object
                                $hash[$k] = ConvertToHashtable($v)
                            }
                            else { $hash[$k] = $v }
                        }
                    }
                    else { $hash = $obj }
                    $hash
                }
                # Recurse
                ConvertToHashtable ($json | ConvertFrom-Json)
            }

            $regex = "(?:`"[^`"]*`"|'[^']*'|\S)+"
            $matches = [regex]::Matches($command_ast.CommandElements, $regex)

            $input_arr = New-Object System.Collections.Generic.List[string]
            foreach ($match in $matches) { $input_arr.Add($match.Value) }

            $alias = $input_arr[0]

            $PSCompletions.current_cmd = $root = $PSCompletions.alias.$alias

            $input_arr.RemoveAt(0)

            # get json
            if (!$PSCompletions.data) {
                $PSCompletions.data = [ordered]@{}
                if ($PSCompletions.job.State -eq 'Completed') {
                    $data = Receive-Job $PSCompletions.job
                    $data.Keys | ForEach-Object {
                        $PSCompletions.data.$_ = $data.$_
                    }
                    Remove-Job $PSCompletions.job
                    $PSCompletions.job = $null
                }
            }
            if (!$PSCompletions.data.$root -or $PSCompletions.config.disable_cache) {
                $language = $PSCompletions.get_language($root)
                $PSCompletions.data.$root = ConvertFrom-JsonToHashtable $PSCompletions.get_raw_content("$($PSCompletions.path.completions)/$($root)/language/$($language).json")
            }

            $common_options = if ($PSCompletions.data.$root.common_options) {
                $PSCompletions.data.$root.common_options | ForEach-Object { $_.name }
            }
            else { New-Object System.Collections.ArrayList }

            $WriteSpaceTab = [System.Collections.Generic.List[string]]@()

            $WriteSpaceTab_and_SpaceTab = [System.Collections.Generic.List[string]]@()

            $alias_map = @{}

            function getCompletions {
                $completions = [System.Collections.Generic.List[System.Object]]@()
                $index = 1
                function parseCompletions ($node, $pre, [switch]$isOption) {
                    $node | ForEach-Object {
                        $pad = if ($pre) { ' ' }else { '' }
                        $symbols = @()
                        if ($isOption) {
                            $symbols += 'OptionTab'
                        }
                        if ($_.next -or $_.options) {
                            $symbols += 'SpaceTab'
                            if ($isOption) {
                                $symbols += 'WriteSpaceTab'
                            }
                        }
                        if ($_.symbol) {
                            $symbols += $PSCompletions.replace_content($_.symbol, ' ') -split ' '
                        }
                        $symbols = $symbols | Select-Object -Unique

                        $completions.Add(@{
                                name   = $pre + $pad + $_.name
                                symbol = $symbols
                                tip    = $_.tip
                            })
                        if ($_.alias) {
                            if ($isOption) {
                                foreach ($a in $_.alias) {
                                    $completions.Add(@{
                                            name   = $pre + $pad + $a
                                            symbol = $symbols
                                            tip    = $_.tip
                                        })
                                    if ($_.next) { parseCompletions $_.next ($pre + $pad + $a) }
                                }
                            }
                            else {
                                foreach ($a in $_.alias) {
                                    # 判断别名出现的位置
                                    $index = (($pre + $pad + $_.name) -split ' ').Length - 1
                                    # 用这个位置创建一个数组,将所有在这个位置出现的别名全部写入这个数组
                                    if (!($alias_map[$index])) { $alias_map[$index] = @() }
                                    $alias_map[$index] += @{
                                        name  = $_.name
                                        alias = $a
                                    }
                                    $completions.Add(@{
                                            name   = $pre + $pad + $a
                                            symbol = $symbols
                                            tip    = $_.tip
                                        })
                                }
                            }
                        }
                        if ($symbols) {
                            if ('WriteSpaceTab' -in $symbols) {
                                $WriteSpaceTab.Add($_.name)
                                if ($_.alias) {
                                    foreach ($a in $_.alias) { $WriteSpaceTab.Add($a) }
                                }
                                if ('SpaceTab' -in $symbols) {
                                    $WriteSpaceTab_and_SpaceTab.Add($_.name)
                                    if ($_.alias) {
                                        foreach ($a in $_.alias) { $WriteSpaceTab_and_SpaceTab.Add($a) }
                                    }
                                }
                            }
                        }
                        if ($_.next) { parseCompletions $_.next ($pre + $pad + $_.name) }
                        if ($_.options) { parseCompletions $_.options ($pre + $pad + $_.name) -isOption }
                    }
                }
                if ($PSCompletions.data.$root.root) { parseCompletions $PSCompletions.data.$root.root '' }
                if ($PSCompletions.data.$root.options) { parseCompletions $PSCompletions.data.$root.options '' -isOption }
                return $completions
            }
            function handleCompletions($completions) { return $completions }
            function filterCompletions($completions, $root) {
                # 是否是按下空格键触发的补全
                $space_tab = if (!$word_to_complete.length) { 1 }else { 0 }

                # 当这个 options 是 WriteSpaceTab 时,将下一个值直接过滤掉
                $need_skip = $false

                $filter_input_arr = [System.Collections.Generic.List[string]]@()
                $input_arr | ForEach-Object {
                    if ($_ -like '-*' -or $need_skip) {
                        if ($need_skip) { $need_skip = $false }
                        if ($_ -in $WriteSpaceTab) {
                            if ($input_arr[-1 - !$space_tab] -eq $_ -and $_ -in $WriteSpaceTab_and_SpaceTab) {
                                $need_add = $true
                            }
                            else {
                                $need_skip = $true
                            }
                        }
                    }
                    else { $need_add = $true }
                    if ($need_add -and $_ -notin $common_options) {
                        $filter_input_arr.Add($_)
                        $need_add = $false
                    }
                }

                if (!$space_tab) {
                    # 如果是输入 -* 过程中触发的补全,则需要把最后一个 -* 加入其中
                    if ($input_arr[-1] -like '-*') {
                        $filter_input_arr += $input_arr[-1]
                    }
                }

                if ($filter_input_arr.Count) {
                    $match = if ($space_tab) { ' *' }else { '*' }
                }
                else {
                    # 如果过滤出来为空,则是只是输入了根命令,没有输入其他内容
                    $match = '*'
                }

                $filter_list = [System.Collections.Generic.List[System.Object]]@()
                $completions | ForEach-Object {
                    $matches = [regex]::Matches($_.name, "(?:`"[^`"]*`"|'[^']*'|\S)+")
                    $cmd = [System.Collections.Generic.List[string]]@()
                    foreach ($m in $matches) { $cmd.Add($m.Value) }
                    <#
                        判断选项是否使用过了,如果使用过了,$no_used 为 $true
                        这里的判断对于 --file="abc" 这样的命令无法使用,因为这里和用户输入的 "abc"是连着的
                    #>

                    $no_used = if ($cmd[-1] -like '-*') {
                        $cmd[-1] -notin $input_arr
                    }
                    else { $true }

                    $alias_input_arr = $filter_input_arr

                    # 循环命令的长度,针对每一个位置去 $alias_map 找到对应的数组,然后把数组里的值拿出来比对,如果有匹配的,替换掉原来的命令名
                    # 用位置的好处是,这样遍历是依赖于命令的长度,而命令长度一般不长
                    for ($i = 0; $i -lt $filter_input_arr.Count; $i++) {
                        if ($alias_map[$i]) {
                            foreach ($obj in $alias_map[$i]) {
                                if ($obj.alias -eq $filter_input_arr[$i]) {
                                    $alias_input_arr[$i] = $obj.name
                                    break
                                }
                            }
                        }
                    }
                    $isLike = ($_.name -like ([WildcardPattern]::Escape($filter_input_arr -join ' ') + $match)) -or ($_.name -like ([WildcardPattern]::Escape($alias_input_arr -join ' ') + $match))

                    if ($no_used -and $cmd.Count -eq ($filter_input_arr.Count + $space_tab) -and $isLike) {
                        # 让 name 的值变成一个数组,方便后面处理
                        $_.name = $cmd
                        $filter_list.Add($_)
                    }
                }

                # 处理 common_options
                if ($PSCompletions.data.$root.common_options) {
                    if ($space_tab) {
                        $PSCompletions.data.$root.common_options | Where-Object { $_.name -notin $input_arr } | ForEach-Object {
                            $symbols = @('OptionTab')
                            if ($_.symbol) {
                                $symbols += $PSCompletions.replace_content($_.symbol, ' ') -split ' '
                            }
                            $symbols = $symbols | Select-Object -Unique
                            if ($_.alias) {
                                foreach ($a in $_.alias) {
                                    $filter_list.Add(@{
                                            name   = @($a)
                                            symbol = $symbols
                                            tip    = $_.tip
                                        })
                                }
                            }
                            $_.name = @($_.name)
                            $_.symbol = $symbols
                            $filter_list.Add($_)
                        }
                    }
                    else {
                        $PSCompletions.data.$root.common_options | Where-Object { $_.name -notin $input_arr -and $_.name -like "$($filter_input_arr[-1])*" } | ForEach-Object {
                            $_.name = @($_.name)
                            $filter_list.Add($_)
                        }
                    }
                }

                return $filter_list
            }

            # 允许外部的 hooks 覆盖默认的函数,实现在特殊需求下的定制需要
            $path_hook = "$($PSCompletions.path.completions)/$($root)/hooks.ps1"
            if (Test-Path $path_hook) { . $path_hook }

            $completions = getCompletions
            $completions = handleCompletions $completions
            $filter_list = filterCompletions $completions $root

            # order
            if ($PSCompletions.config.menu_completions_sort) {
                $path_order = "$($PSCompletions.path.completions)/$($root)/order.json"
                if ($PSCompletions.order."$($root)_job") {
                    if ($PSCompletions.order."$($root)_job".State -eq 'Completed') {
                        $PSCompletions.order.$root = Receive-Job $PSCompletions.order."$($root)_job"
                        Remove-Job $PSCompletions.order."$($root)_job"
                        $PSCompletions.order.Remove("$($root)_job")
                    }
                }
                else {
                    # 第一次运行,直接获取排序
                    if (Test-Path $path_order) {
                        $PSCompletions.order.$root = ConvertFrom-JsonToHashtable $PSCompletions.get_raw_content($path_order)
                    }
                    else {
                        # 没有使用过此命令,还没有命令排序
                        $PSCompletions.order.$root = $null
                    }
                }
                $order = $PSCompletions.order.$root
                if ($order) {
                    $filter_list = $filter_list | Sort-Object {
                        $o = $order.$($_.name -join ' ')
                        if ($o) { $o }else { 999999999 }
                    }
                }

                $PSCompletions.order."$($root)_job" = Start-Job -ScriptBlock {
                    param($PScompletions, $completions, $path_history, $root, $path_order)
                    $order = [ordered]@{}
                    $index = 1
                    $completions | ForEach-Object {
                        $order.$($_.name -join ' ') = $index
                        $index++
                    }
                    $historys = [System.Collections.Generic.List[string]]@()
                    Get-Content $path_history -Encoding utf8 -ErrorAction SilentlyContinue | ForEach-Object {
                        foreach ($alias in $PSCompletions.cmd.$root) {
                            if ($_ -match "^[^\S\n]*$($alias)\s+.+") {
                                $historys.Add($_)
                                break
                            }
                        }
                    }
                    $index = -1
                    function handle_order([array]$history) {
                        $str = $history -join ' '
                        if ($str -in $order.Keys) {
                            $order.$str = $index
                        }
                        if ($history.Count -eq 1) {
                            return
                        }
                        else {
                            handle_order $history[0..($history.Count - 2)]
                        }
                    }

                    $historys | ForEach-Object {
                        $matches = [regex]::Matches($_, "(?:`"[^`"]*`"|'[^']*'|\S)+")
                        $cmd = [System.Collections.Generic.List[string]]@()
                        foreach ($m in $matches) { $cmd.Add($m.Value) }
                        if ($cmd.Count -gt 1) {
                            handle_order $cmd[1..($cmd.Count - 1)]
                            $index--
                        }
                    }
                    $json = $order | ConvertTo-Json -Compress
                    $matches = [regex]::Matches($json, '\s*"\s*"\s*:')
                    foreach ($match in $matches) {
                        $json = $json -replace $match.Value, "`"empty_key_$([System.Guid]::NewGuid().Guid)`":"
                    }
                    $json | Out-File $path_order -Encoding utf8 -Force
                    return $order
                } -ArgumentList $PScompletions, $completions, (Get-PSReadLineOption).HistorySavePath, $root, $path_order
            }

            $menu_show_tip = $PSCompletions.config.comp_config.$root.menu_show_tip
            if ($menu_show_tip) {
                $PSCompletions.menu.is_show_tip = $menu_show_tip -eq 1
            }
            else {
                $PSCompletions.menu.is_show_tip = $PSCompletions.config.menu_show_tip -eq 1
            }
            $PSCompletions.menu.show_powershell_menu($filter_list)
        }
    }
}