core/completion/unix.ps1

Add-Member -InputObject $PSCompletions -MemberType ScriptMethod generate_completion {}
Add-Member -InputObject $PSCompletions -MemberType ScriptMethod handle_completion {
    foreach ($_ in $this.alias.keys) {
        Register-ArgumentCompleter -CommandName $_ -ScriptBlock {
            param($word_to_complete, $command_ast, $cursor_position)

            # 使用正则表达式进行分割,将命令行中的每个参数分割出来,形成一个数组, 引号包裹的内容会被当作一个参数,且数组会包含 "--"
            $input_arr = [System.Collections.Generic.List[string]]@()
            $matches = [regex]::Matches($command_ast.CommandElements, "(?:`"[^`"]*`"|'[^']*'|\S)+")
            foreach ($match in $matches) { $input_arr.Add($match.Value) }

            # 触发补全的值,此值可能是别名或命令名
            $alias = $input_arr[0]

            # 原始的命令名,也是 completions 目录下的命令目录名
            $PSCompletions.current_cmd = $root = $PSCompletions.alias.$alias

            $input_arr.RemoveAt(0)

            # 是否是按下空格键触发的补全
            $space_tab = if (!$word_to_complete.length) { 1 }else { 0 }

            # 获取 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"))
            }

            $common_options = [System.Collections.Generic.List[string]]@()
            $common_options_with_next = [System.Collections.Generic.List[string]]@()
            if ($PSCompletions.data.$root.common_options) {
                foreach ($_ in $PSCompletions.data.$root.common_options) {
                    foreach ($a in $_.alias) {
                        $common_options.Add($a)
                        if ($_.next) { $common_options_with_next.Add($a) }
                    }
                    $common_options.Add($_.name)
                    if ($_.next) { $common_options_with_next.Add($_.name) }
                }
            }
            $WriteSpaceTab = [System.Collections.Generic.List[string]]@()
            $WriteSpaceTab.AddRange($common_options_with_next)

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

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

            function getCompletions {
                $completions = [System.Collections.Generic.List[System.Object]]@()

                $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]]@()
                            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
                                    $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 ($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 }
                                }
                            }
                            parseCompletions $obj.node '' $obj.isOption
                            return $completions
                        }).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.AddRange($result)
                }
                return $completions
            }
            function handleCompletions {
                param($completions)
                return $completions
            }
            function filterCompletions {
                param($completions, $root)

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

                $filter_input_arr = [System.Collections.Generic.List[string]]@()
                foreach ($_ in $input_arr) {
                    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 = '*'
                }

                $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 = [System.Collections.Generic.List[System.Object]]@()

                $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)
                            foreach ($completion in $completions) {
                                $matches = [regex]::Matches($completion.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 }


                                $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) {
                                    $completion
                                }
                            }
                        }).AddArgument($completions).AddArgument($input_arr).AddArgument($filter_input_arr).AddArgument($match).AddArgument($alias_input_arr).AddArgument($space_tab)


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

                # 等待所有任务完成
                foreach ($rs in $runspaces) {
                    $result = $rs.Runspace.EndInvoke($rs.Job)
                    $rs.Runspace.Dispose()
                    if ($result) {
                        $filter_list.AddRange($result)
                    }
                }

                # 处理 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.Clear()
                            $PSCompletions.data.$root.common_options | Where-Object {
                                $_.name -eq $input_arr[-1] -or $_.alias -contains $input_arr[-1]
                            } | ForEach-Object {
                                foreach ($n in $_.next) {
                                    $filter_list.Add(@{
                                            ListItemText   = $n.name
                                            CompletionText = $n.name
                                            ToolTip        = $n.tip
                                        })
                                }
                            }
                        }
                        foreach ($_ in $PSCompletions.data.$root.common_options) {
                            if ($_.name -notin $input_arr) {
                                $isExist = $false
                                $temp_list = [System.Collections.Generic.List[System.Object]]@()

                                $temp_list.Add(@{
                                        ListItemText   = "$($_.name)$(Get-PadSymbols)"
                                        CompletionText = $_.name
                                        ToolTip        = $_.tip
                                    })

                                foreach ($a in $_.alias) {
                                    if ($a -notin $input_arr) {
                                        $temp_list.Add(@{
                                                ListItemText   = "$($a)$(Get-PadSymbols)"
                                                CompletionText = $a
                                                ToolTip        = $_.tip
                                            })
                                    }
                                    else {
                                        $temp_list.Clear()
                                        break
                                    }
                                }
                                $filter_list.AddRange($temp_list)
                            }
                        }
                    }
                    else {
                        if ($input_arr[-2] -in $common_options_with_next -and $input_arr -notlike "*$($input_arr[-2])*$($input_arr[-2])*") {
                            $filter_list.Clear()
                            $PSCompletions.data.$root.common_options | Where-Object {
                                $_.name -eq $input_arr[-2] -or $_.alias -contains $input_arr[-2]
                            } | ForEach-Object {
                                foreach ($n in $_.next) {
                                    if ($n.name -like "$($input_arr[-1])*") {
                                        $filter_list.Add(@{
                                                ListItemText   = $n.name
                                                CompletionText = $n.name
                                                ToolTip        = $n.tip
                                            })
                                    }
                                }
                            }
                        }
                        foreach ($_ in $PSCompletions.data.$root.common_options) {
                            if ($_.name -notin $input_arr -and $_.name -like "$($input_arr[-1])*") {
                                $filter_list.Add(@{
                                        ListItemText   = "$($_.name)$(Get-PadSymbols)"
                                        CompletionText = $_.name
                                        ToolTip        = $_.tip
                                    })
                            }
                            foreach ($a in $_.alias) {
                                if ($a -notin $input_arr -and $a -like "$($input_arr[-1])*") {
                                    $filter_list.Add(@{
                                            ListItemText   = "$($a)$(Get-PadSymbols)"
                                            CompletionText = $a
                                            ToolTip        = $_.tip
                                        })
                                }
                            }
                        }
                    }
                }
                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)
            }
            $PSCompletions.menu.show_powershell_menu($filter_list)
            $PSCompletions.current_cmd = $null
        }
    }
}