core/completion/utils.ps1

Add-Member -InputObject $PSCompletions -MemberType ScriptMethod get_completion {
    # 获取 json 数据
    if ($PSCompletions.job.State -eq 'Completed') {
        $data = Receive-Job $PSCompletions.job
        foreach ($_ in $data.Keys) {
            $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 = $PSCompletions.ConvertFrom_JsonToHashtable($PSCompletions.get_raw_content("$($PSCompletions.path.completions)/$($root)/language/$($language).json"))
    }

    $input_arr = [array]$input_arr

    $common_options = @()
    $common_options_with_next = @()
    if ($PSCompletions.data.$root.common_options) {
        foreach ($_ in $PSCompletions.data.$root.common_options) {
            foreach ($a in $_.alias) {
                $common_options += $a
                if ($_.next) { $common_options_with_next += $a }
            }
            $common_options += $_.name
            if ($_.next) { $common_options_with_next += $_.name }
        }
    }
    $PSCompletions.temp_WriteSpaceTab = @()
    $PSCompletions.temp_WriteSpaceTab += $common_options_with_next

    $PSCompletions.temp_WriteSpaceTab_and_SpaceTab = @()

    # 存储别名的映射,用于在过滤时允许别名
    $alias_map = @{}

    function getCompletions {
        $completions = @()

        $runspacePool = [runspacefactory]::CreateRunspacePool(1, [Environment]::ProcessorCount)
        $runspacePool.Open()
        $runspaces = @()

        $tasks = @()
        if ($PSCompletions.data.$root.root) {
            $tasks += @{
                node     = $PSCompletions.data.$root.root
                isOption = $false
            }
        }
        if ($PSCompletions.data.$root.options) {
            $tasks += @{
                node     = $PSCompletions.data.$root.options
                isOption = $true
            }
        }
        foreach ($task in $tasks) {
            $runspace = [powershell]::Create().AddScript({
                    param($obj, $PSCompletions)
                    $completions = [System.Collections.Generic.List[System.Object]]@()
                    $_alias_map = @{}
                    $temp = @{
                        WriteSpaceTab              = @()
                        WriteSpaceTab_and_SpaceTab = @()
                    }
                    function _replace {
                        param ($data, $separator = '')
                        $data = ($data -join $separator)
                        $pattern = '\{\{(.*?(\})*)(?=\}\})\}\}'
                        $matches = [regex]::Matches($data, $pattern)
                        foreach ($match in $matches) {
                            $data = $data.Replace($match.Value, (Invoke-Expression $match.Groups[1].Value) -join $separator )
                        }
                        if ($data -match $pattern) { (_replace $data) }else { return $data }
                    }
                    function parseCompletions {
                        param($node, $pre, $isOption)
                        foreach ($_ in $node) {
                            $pad = if ($pre) { ' ' }else { '' }
                            $symbols = @()
                            if ($isOption) {
                                $symbols += 'OptionTab'
                            }
                            if ($_.next -or $_.options) {
                                $symbols += 'SpaceTab'
                                if ($isOption) {
                                    $symbols += 'WriteSpaceTab'
                                }
                            }
                            if ($_.symbol) {
                                $symbols += (_replace $_.symbol ' ') -split ' '
                            }
                            $symbols = $symbols | Select-Object -Unique

                            if ($symbols) {
                                if ('WriteSpaceTab' -in $symbols) {
                                    $temp.WriteSpaceTab += $_.name
                                    if ($_.alias) {
                                        foreach ($a in $_.alias) { $temp.WriteSpaceTab += $a }
                                    }
                                    if ('SpaceTab' -in $symbols) {
                                        $temp.WriteSpaceTab_and_SpaceTab += $_.name
                                        if ($_.alias) {
                                            foreach ($a in $_.alias) { $temp.WriteSpaceTab_and_SpaceTab += $a }
                                        }
                                    }
                                }
                            }

                            $symbols = foreach ($c in $symbols) { $PSCompletions.config."symbol_$($c)" }
                            $symbols = $symbols -join ''
                            $padSymbols = if ($symbols) { "$($PSCompletions.config.menu_between_item_and_symbol)$($symbols)" }else { '' }

                            $completions.Add(@{
                                    name           = $pre + $pad + $_.name
                                    ListItemText   = "$($_.name)$($padSymbols)"
                                    CompletionText = $_.name
                                    ToolTip        = $_.tip
                                })
                            if ($_.alias) {
                                if ($isOption) {
                                    foreach ($a in $_.alias) {
                                        $completions.Add(@{
                                                name           = $pre + $pad + $a
                                                ListItemText   = "$($a)$($padSymbols)"
                                                CompletionText = $a
                                                ToolTip        = $_.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
                                                ListItemText   = "$($a)$($padSymbols)"
                                                CompletionText = $a
                                                ToolTip        = $_.tip
                                            })
                                    }
                                }
                            }
                            if ($_.next) { parseCompletions $_.next ($pre + $pad + $_.name) }
                            if ($_.options) { parseCompletions $_.options ($pre + $pad + $_.name) $true }
                        }
                    }
                    parseCompletions $obj.node '' $obj.isOption
                    @{
                        completions = $completions
                        alias_map   = $_alias_map
                        temp        = $temp
                    }
                }).AddArgument($task).AddArgument($PSCompletions)
            $runspace.RunspacePool = $runspacePool
            $runspaces += @{ Runspace = $runspace; Job = $runspace.BeginInvoke() }
        }

        # 等待所有任务完成
        foreach ($rs in $runspaces) {
            $result = $rs.Runspace.EndInvoke($rs.Job)
            $rs.Runspace.Dispose()
            $completions += $result.completions
            $PSCompletions.temp_WriteSpaceTab += $result.temp.WriteSpaceTab
            $PSCompletions.temp_WriteSpaceTab_and_SpaceTab += $result.temp.WriteSpaceTab_and_SpaceTab
            foreach ($a in $result.alias_map.Keys) {
                if ($alias_map[$a]) {
                    $alias_map[$a] += $result.alias_map[$a]
                }
                else {
                    $alias_map[$a] = $result.alias_map[$a]
                }
            }
        }
        return $completions
    }
    function handleCompletions {
        param($completions)
        return $completions
    }
    function filterCompletions {
        param($completions, $root)

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

        $filter_input_arr = @()
        foreach ($_ in $input_arr) {
            if ($_ -like '-*' -or $need_skip) {
                if ($need_skip) { $need_skip = $false }
                if ($_ -in $PSCompletions.temp_WriteSpaceTab) {
                    if ($input_arr[-1 - !$space_tab] -eq $_ -and $_ -in $PSCompletions.temp_WriteSpaceTab_and_SpaceTab) {
                        $need_add = $true
                    }
                    else {
                        $need_skip = $true
                    }
                }
            }
            else { $need_add = $true }
            if ($need_add -and $_ -notin $common_options) {
                $filter_input_arr += $_
                $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 = '*'
        }

        $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
                    }
                }
            }
        }

        $filter_list = @()

        $runspacePool = [runspacefactory]::CreateRunspacePool(1, [Environment]::ProcessorCount)
        $runspacePool.Open()
        $runspaces = @()

        foreach ($completions in $PSCompletions.split_array($completions, [Environment]::ProcessorCount, $true)) {
            $runspace = [powershell]::Create().AddScript({
                    param($completions, $input_arr, $filter_input_arr, $match, $alias_input_arr, $space_tab, $host_ui)
                    $max_width = 0
                    $results = @()
                    function get_length {
                        param([string]$str)
                        $host_ui.RawUI.NewBufferCellArray($str, $host_ui.RawUI.BackgroundColor, $host_ui.RawUI.BackgroundColor).LongLength
                    }
                    foreach ($completion in $completions) {
                        $matches = [regex]::Matches($completion.name, "(?:`"[^`"]*`"|'[^']*'|\S)+")
                        $cmd = @()
                        foreach ($m in $matches) { $cmd += $m.Value }

                        # 判断选项是否使用过了,如果使用过了,$no_used 为 $true
                        # 这里的判断对于 --file="abc" 这样的命令无法使用,因为这里和用户输入的 "abc"是连着的
                        $no_used = if ($cmd[-1] -like '-*') {
                            $cmd[-1] -notin $input_arr
                        }
                        else { $true }

                        $isLike = ($completion.name -like ([WildcardPattern]::Escape($filter_input_arr -join ' ') + $match)) -or ($completion.name -like ([WildcardPattern]::Escape($alias_input_arr -join ' ') + $match))
                        if ($no_used -and $cmd.Count -eq ($filter_input_arr.Count + $space_tab) -and $isLike) {
                            $results += $completion
                            $max_width = [Math]::Max($max_width, (get_length $completion.ListItemText))
                        }
                    }
                    @{
                        results   = $results
                        max_width = $max_width
                    }
                }).AddArgument($completions).AddArgument($input_arr).AddArgument($filter_input_arr).AddArgument($match).AddArgument($alias_input_arr).AddArgument($space_tab).AddArgument($Host.UI)


            $runspace.RunspacePool = $runspacePool
            $runspaces += @{ Runspace = $runspace; Job = $runspace.BeginInvoke() }
        }

        # 等待所有任务完成
        foreach ($rs in $runspaces) {
            $result = $rs.Runspace.EndInvoke($rs.Job)
            $rs.Runspace.Dispose()
            if ($result.results) {
                $filter_list += $result.results
            }
            $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $result.max_width)
        }

        # 处理 common_options
        if ($PSCompletions.data.$root.common_options) {
            function Get-PadSymbols {
                $symbols = @('OptionTab')
                if ($_.next) {
                    $symbols += 'SpaceTab'
                    $symbols += 'WriteSpaceTab'
                }
                if ($_.symbol) {
                    $symbols += $PSCompletions.replace_content($_.symbol, ' ') -split ' '
                }
                $symbols = $symbols | Select-Object -Unique

                $symbols = foreach ($c in $symbols) { $PSCompletions.config."symbol_$($c)" }
                $symbols = $symbols -join ''
                if ($symbols) {
                    "$($PSCompletions.config.menu_between_item_and_symbol)$($symbols)"
                }
                else {
                    ''
                }
            }
            if ($space_tab) {
                if ($input_arr[-1] -in $common_options_with_next -and ($input_arr -notlike "*$($input_arr[-1])*$($input_arr[-1])*" -or $input_arr -like "*$($input_arr[-1])")) {
                    $filter_list = @()
                    $filter = $PSCompletions.data.$root.common_options.Where({ $_.name -eq $input_arr[-1] -or $_.alias -contains $input_arr[-1] })
                    foreach ($_ in $filter) {
                        foreach ($n in $_.next) {
                            $filter_list += @{
                                ListItemText   = $n.name
                                CompletionText = $n.name
                                ToolTip        = $n.tip
                            }
                            $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $PSCompletions.menu.get_length($n.name))
                        }
                    }
                }
                foreach ($_ in $PSCompletions.data.$root.common_options) {
                    if ($_.name -notin $input_arr) {
                        $isExist = $false
                        $temp_list = @()
                        $name_with_symbol = "$($_.name)$(Get-PadSymbols)"
                        $temp_list += @{
                            ListItemText   = $name_with_symbol
                            CompletionText = $_.name
                            ToolTip        = $_.tip
                        }
                        $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $PSCompletions.menu.get_length($name_with_symbol))

                        foreach ($a in $_.alias) {
                            if ($a -notin $input_arr) {
                                $name_with_symbol = "$($a)$(Get-PadSymbols)"
                                $temp_list += @{
                                    ListItemText   = $name_with_symbol
                                    CompletionText = $a
                                    ToolTip        = $_.tip
                                }
                                $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $PSCompletions.menu.get_length($name_with_symbol))
                            }
                            else {
                                $temp_list = @()
                                break
                            }
                        }
                        $filter_list += $temp_list
                    }
                }
            }
            else {
                if ($input_arr[-2] -in $common_options_with_next -and $input_arr -notlike "*$($input_arr[-2])*$($input_arr[-2])*") {
                    $filter_list = @()
                    $filter = $PSCompletions.data.$root.common_options.Where({ $_.name -eq $input_arr[-2] -or $_.alias -contains $input_arr[-2] })
                    foreach ($_ in $filter) {
                        foreach ($n in $_.next) {
                            if ($n.name -like "$($input_arr[-1])*") {
                                $filter_list += @{
                                    ListItemText   = $n.name
                                    CompletionText = $n.name
                                    ToolTip        = $n.tip
                                }
                                $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $PSCompletions.menu.get_length($n.name))
                            }
                        }
                    }
                }
                foreach ($_ in $PSCompletions.data.$root.common_options) {
                    if ($_.name -notin $input_arr -and $_.name -like "$($input_arr[-1])*") {
                        $name_with_symbol = "$($_.name)$(Get-PadSymbols)"
                        $filter_list += @{
                            ListItemText   = $name_with_symbol
                            CompletionText = $_.name
                            ToolTip        = $_.tip
                        }
                        $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $PSCompletions.menu.get_length($name_with_symbol))
                    }
                    foreach ($a in $_.alias) {
                        if ($a -notin $input_arr -and $a -like "$($input_arr[-1])*") {
                            $name_with_symbol = "$($a)$(Get-PadSymbols)"
                            $filter_list += @{
                                ListItemText   = $name_with_symbol
                                CompletionText = $a
                                ToolTip        = $_.tip
                            }
                            $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $PSCompletions.menu.get_length($name_with_symbol))
                        }
                    }
                }
            }
        }
        return $filter_list
    }

    if ($PSCompletions.config.comp_config.$root.disable_hooks -ne 1) {
        # 使用 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

    # 排序
    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 = $PSCompletions.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_job($completions, (Get-PSReadLineOption).HistorySavePath, $root, $path_order)
    }
    return $filter_list
}