core/utils/Core.ps1

Add-Member -InputObject $PSCompletions -MemberType ScriptMethod ConvertFrom_JsonToHashtable {
    param([string]$json)
    ConvertFrom-Json $json -AsHashtable
}
Add-Member -InputObject $PSCompletions -MemberType ScriptMethod start_job {
    # Start-Job 传入的对象的方法会丢失
    # Start-ThreadJob 不会,但是耗时方法执行会卡住主线程

    $PSCompletions.job = Start-ThreadJob -ScriptBlock {
        param($PSCompletions)

        $null = Start-ThreadJob -ScriptBlock {
            param($PSCompletions)
            $wc = New-Object System.Net.WebClient

            function get_raw_content {
                param ([string]$path, [bool]$trim = $true)
                $res = Get-Content $path -Raw -Encoding utf8 -ErrorAction SilentlyContinue
                if ($res) {
                    if ($trim) { return $res.Trim() }
                    return $res
                }
                ''
            }
            function replace_content {
                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_content $data) }else { return $data }
            }
            function download_file {
                param(
                    [string]$path, # 相对于 $baseUrl 的文件路径
                    [string]$file,
                    [array]$baseUrl
                )
                $isErr = $true
                for ($i = 0; $i -lt $baseUrl.Count; $i++) {
                    $item = $baseUrl[$i]
                    $url = $item + '/' + $path
                    try {
                        $wc.DownloadFile($url, $file)
                        $isErr = $false
                        break
                    }
                    catch {}
                }
                if ($isErr) {
                    throw
                }
            }
            function ensure_dir {
                param([string]$path)
                if (!(Test-Path $path)) { New-Item -ItemType Directory $path > $null }
            }
            function ensure_psc {
                ensure_dir "$($PSCompletions.path.completions)/psc"
                ensure_dir "$($PSCompletions.path.completions)/psc/language"

                $path_config = "$($PSCompletions.path.completions)/psc/config.json"
                download_file "completions/psc/config.json" $path_config $PSCompletions.urls

                $config = get_raw_content $path_config | ConvertFrom-Json

                $file_list = @('guid.txt')
                if ($config.hooks -ne $null) {
                    $file_list += 'hooks.ps1'
                }
                foreach ($lang in $config.language) {
                    $file_list += "language/$lang.json"
                }
                foreach ($path in $file_list) {
                    $path_file = "$($PSCompletions.path.completions)/psc/$path"
                    if (!(Test-Path $path_file)) {
                        download_file "completions/psc/$path" $path_file $PSCompletions.urls
                    }
                }
            }
            function download_list {
                ensure_dir $PSCompletions.path.temp
                if (!(Test-Path $PSCompletions.path.completions_json)) {
                    @{ list = @('psc') } | ConvertTo-Json -Compress | Out-File $PSCompletions.path.completions_json -Encoding utf8 -Force
                }
                $current_list = (get_raw_content $PSCompletions.path.completions_json | ConvertFrom-Json).list
                foreach ($url in $PSCompletions.urls) {
                    try {
                        $content = (Invoke-WebRequest -Uri "$url/completions.json").Content | ConvertFrom-Json

                        $remote_list = $content.list

                        $diff = Compare-Object $remote_list $current_list -PassThru
                        if ($diff) {
                            $diff | Out-File $PSCompletions.path.change -Force -Encoding utf8
                            $content | ConvertTo-Json -Depth 100 -Compress | Out-File $PSCompletions.path.completions_json -Encoding utf8 -Force
                            $PSCompletions.list = $remote_list
                        }
                        else {
                            Clear-Content $PSCompletions.path.change -Force
                            $PSCompletions.list = $current_list
                        }
                        $isErr = $false
                        return $remote_list
                    }
                    catch {}
                }
            }

            ensure_dir $PSCompletions.path.order

            ensure_psc

            $null = download_list

            # data.json
            $data = [ordered]@{
                list     = @()
                alias    = [ordered]@{}
                aliasMap = [ordered]@{}
                config   = [ordered]@{}
            }
            $data.config.comp_config = [ordered]@{}
            foreach ($_ in Get-ChildItem $PSCompletions.path.completions -Directory) {
                $cmd = $_.Name
                $data.list += $cmd

                $data.alias.$cmd = @()
                if ($PSCompletions.data.alias[$cmd] -ne $null) {
                    foreach ($a in $PSCompletions.data.alias.$cmd) {
                        $data.alias.$cmd += $a
                        $data.aliasMap.$a = $cmd
                    }
                }
                else {
                    $data.alias.$cmd += $cmd
                    $data.aliasMap.$cmd = $cmd
                }

                ## config.comp_config
                $completion = $cmd
                if ($data.config.comp_config[$completion] -eq $null) {
                    $data.config.comp_config.$completion = [ordered]@{}
                }
                foreach ($c in $PSCompletions.config.comp_config.$completion.Keys) {
                    $data.config.comp_config.$completion.$c = $PSCompletions.config.comp_config.$completion.$c
                }
            }

            ## config
            foreach ($c in $PSCompletions.default_config.Keys) {
                if ($PSCompletions.config[$c] -ne $null) {
                    $data.config.$c = $PSCompletions.config.$c
                }
                else {
                    $data.config.$c = $PSCompletions.default_config.$c
                }
            }

            foreach ($_ in $PSCompletions.data.list) {
                $path = "$($PSCompletions.path.completions)/$_/config.json"
                if (!(Test-Path $path)) {
                    try {
                        download_file "completions/$_/config.json" $path $PSCompletions.urls
                    }
                    catch {
                        continue
                    }
                }
                ensure_dir "$($PSCompletions.path.completions)/$_/language"
                $json_config = get_raw_content $path | ConvertFrom-Json
                foreach ($lang in $json_config.language) {
                    $path_lang = "$($PSCompletions.path.completions)/$_/language/$lang.json"
                    if (!(Test-Path $path_lang)) {
                        download_file "completions/$_/language/$lang.json" $path_lang $PSCompletions.urls
                    }
                }
                if ($json_config.hooks -ne $null) {
                    $path_hooks = "$($PSCompletions.path.completions)/$_/hooks.ps1"
                    if (!(Test-Path $path_hooks)) {
                        download_file "completions/$_/hooks.ps1" $path_hooks $PSCompletions.urls
                    }
                }
                $path = "$($PSCompletions.path.completions)/$_/language/$($json_config.language[0]).json"
                $json = get_raw_content $path | ConvertFrom-Json -AsHashtable
                $config_list = $PSCompletions.default_completion_item
                foreach ($item in $config_list) {
                    if ($data.config.comp_config[$_].$($item.name) -eq '') {
                        $data.config.comp_config[$_].Remove($item.name)
                    }
                }
                foreach ($item in $json.config) {
                    $config_list += $item.name
                    if ($data.config.comp_config[$_] -eq $null) {
                        $data.config.comp_config[$_] = [ordered]@{}
                    }
                    if ($data.config.comp_config[$_].$($item.name) -eq $null) {
                        $data.config.comp_config[$_].$($item.name) = $item.value
                    }
                    else {
                        if ($data.config.comp_config[$_].$($item.name) -eq '' -and $item.value -ne '' -and '' -notin $item.values) {
                            $data.config.comp_config[$_].$($item.name) = $item.value
                        }
                    }
                }
                foreach ($r in $data.config.comp_config[$_].Keys.Where({ $_ -notin $config_list })) {
                    if ($r -eq 'enable_hooks') {
                        if ($json_config.hooks -eq $null) {
                            $data.config.comp_config[$_].Remove($r)
                        }
                    }
                    else {
                        $data.config.comp_config[$_].Remove($r)
                    }
                }
            }
            $_keys = @()
            foreach ($k in $data.config.comp_config.Keys) {
                if (!$data.config.comp_config[$k].Count) {
                    $_keys += $k
                }
            }
            foreach ($_ in $_keys) {
                $data.config.comp_config.Remove($_)
            }

            $new_data = $data | ConvertTo-Json -Depth 100 -Compress
            $old_data = get_raw_content $PSCompletions.path.data | ConvertFrom-Json | ConvertTo-Json -Depth 100 -Compress
            if ($new_data -ne $old_data) {
                $new_data | Out-File $PScompletions.path.data -Force -Encoding utf8
            }

            # check version
            $urls = @('https://pscompletions.pages.dev/version.txt', 'https://abgox.github.io/PSCompletions/module/version.txt') + $PSCompletions.urls
            try {
                if ($PSCompletions.config.enable_module_update -eq 1) {
                    foreach ($url in $urls) {
                        try {
                            $response = Invoke-WebRequest -Uri "$url/module/version.txt"
                            break
                        }
                        catch {}
                    }

                    $content = $response.Content.Trim()
                    if ($content -match "^[\d\.]+$") {
                        $versions = @($PSCompletions.version, $content) | Sort-Object { [Version] $_ }
                        if ($versions[-1] -ne $PSCompletions.version) {
                            $data = get_raw_content $PSCompletions.path.data | ConvertFrom-Json -AsHashtable
                            $data.config.enable_module_update = $versions[-1]
                            $data | ConvertTo-Json -Depth 100 -Compress | Out-File $PSCompletions.path.data -Force -Encoding utf8
                        }
                    }
                }
            }
            catch {}

            # check update
            if (!(Test-Path $PSCompletions.path.update)) {
                New-Item $PSCompletions.path.update -Force -ErrorAction SilentlyContinue
            }
            if ($PSCompletions.config.enable_completions_update -eq 1) {
                $update_list = @()
                foreach ($_ in (Get-ChildItem $PSCompletions.path.completions -ErrorAction SilentlyContinue).Where({ $_.Name -in $PSCompletions.list })) {
                    $isErr = $true
                    foreach ($url in $PSCompletions.urls) {
                        try {
                            $response = Invoke-WebRequest -Uri "$url/completions/$($_.Name)/guid.txt"
                            $isErr = $false
                            break
                        }
                        catch {}
                    }
                    if ($isErr) {
                        continue
                    }
                    try {
                        $content = $response.Content.Trim()
                        $guid = get_raw_content "$($PSCompletions.path.completions)/$($_.Name)/guid.txt"
                        if ($guid -ne $content -and $content -match "^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$") {
                            $update_list += $_.Name
                        }
                    }
                    catch {}
                }
                if ($update_list) { $update_list | Out-File $PSCompletions.path.update -Force -Encoding utf8 }
                else { Clear-Content $PSCompletions.path.update -Force }
            }
        } -ArgumentList $PSCompletions

        $wc = New-Object System.Net.WebClient
        function get_raw_content {
            param ([string]$path, [bool]$trim = $true)
            $res = Get-Content $path -Raw -Encoding utf8 -ErrorAction SilentlyContinue
            if ($res) {
                if ($trim) { return $res.Trim() }
                return $res
            }
            ''
        }
        function replace_content {
            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_content $data) }else { return $data }
        }
        function download_file {
            param(
                [string]$path, # 相对于 $baseUrl 的文件路径
                [string]$file,
                [array]$baseUrl
            )
            $isErr = $true

            for ($i = 0; $i -lt $baseUrl.Count; $i++) {
                $item = $baseUrl[$i]
                $url = $item + '/' + $path
                try {
                    $wc.DownloadFile($url, $file)
                    $isErr = $false
                    break
                }
                catch {}
            }
            if ($isErr) {
                throw
            }
        }
        function ensure_dir {
            param([string]$path)
            if (!(Test-Path $path)) { New-Item -ItemType Directory $path > $null }
        }
        function getCompletions {
            $guid = $PSCompletions.guid
            $obj = @{}
            $special_options = @{
                WriteSpaceTab              = @()
                WriteSpaceTab_and_SpaceTab = @()
            }
            function parseJson($cmds, $obj, $cmdO, [switch]$isOption) {
                $obj.$cmdO = @{
                    $guid = @()
                }
                foreach ($cmd in $cmds) {
                    $symbols = @()
                    if ($isOption) {
                        $symbols += 'OptionTab'
                    }
                    if ($cmd.next -is [array] -or $cmd.options -is [array]) {
                        if ($isOption) {
                            $symbols += 'WriteSpaceTab'
                        }
                        if ($cmd.next.Count -or $cmd.options.Count) {
                            $symbols += 'SpaceTab'
                        }
                    }
                    if ($cmd.symbol) {
                        $symbols += (replace_content $cmd.symbol ' ') -split ' '
                        $symbols = $symbols | Select-Object -Unique
                    }
                    $alias_list = $cmd.alias + $cmd.name

                    $obj.$cmdO.$guid += @{
                        CompletionText = $cmd.name
                        ListItemText   = $cmd.name
                        ToolTip        = $cmd.tip
                        symbols        = $symbols
                        alias          = $alias_list
                    }

                    foreach ($alias in $cmd.alias) {
                        $obj.$cmdO.$guid += @{
                            CompletionText = $alias
                            ListItemText   = $alias
                            ToolTip        = $cmd.tip
                            symbols        = $symbols
                            alias          = $alias_list
                        }
                    }

                    if ($symbols) {
                        if ('WriteSpaceTab' -in $symbols) {
                            $pad = if ($cmdO -in @('rootOptions', 'commonOptions')) { ' ' }else { $cmdO + ' ' }
                            $special_options.WriteSpaceTab += $pad + $cmd.name
                            if ($cmd.alias) {
                                foreach ($a in $cmd.alias) { $special_options.WriteSpaceTab += $pad + $a }
                            }
                            if ('SpaceTab' -in $symbols) {
                                $special_options.WriteSpaceTab_and_SpaceTab += $pad + $cmd.name
                                if ($cmd.alias) {
                                    foreach ($a in $cmd.alias) { $special_options.WriteSpaceTab_and_SpaceTab += $pad + $a }
                                }
                            }
                        }
                    }
                    if ($cmd.options) {
                        parseJson $cmd.options $obj.$cmdO "$($cmd.name)" -isOption
                        foreach ($alias in $cmd.alias) {
                            parseJson $cmd.options $obj.$cmdO "$($alias)" -isOption
                        }
                    }
                    if ($cmd.next -or 'WriteSpaceTab' -in $symbols) {
                        parseJson $cmd.next $obj.$cmdO "$($cmd.name)"
                        foreach ($alias in $cmd.alias) {
                            parseJson $cmd.next $obj.$cmdO "$($alias)"
                        }
                    }
                }
            }
            if ($_completions[$root].root) {
                parseJson $_completions[$root].root $obj 'root'
            }
            if ($_completions[$root].options) {
                parseJson $_completions[$root].options $obj 'rootOptions' -isOption
            }
            if ($_completions[$root].common_options) {
                parseJson $_completions[$root].common_options $obj 'commonOptions' -isOption
            }
            $_completions_data."$($root)_WriteSpaceTab" = $special_options.WriteSpaceTab | Select-Object -Unique
            $_completions_data."$($root)_WriteSpaceTab_and_SpaceTab" = $special_options.WriteSpaceTab_and_SpaceTab | Select-Object -Unique
            $_completions_data."$($root)_common_options" = $obj.commonOptions.$guid | ForEach-Object { $_.CompletionText }
            return $obj
        }
        function get_language {
            param ([string]$completion)

            $path_config = "$($PSCompletions.path.completions)/$completion/config.json"
            $content_config = get_raw_content $path_config | ConvertFrom-Json

            if (!$content_config.language) {
                download_file "completions/$completion/config.json" $path_config $PSCompletions.urls
                $content_config = get_raw_content $path_config | ConvertFrom-Json
            }
            ensure_dir "$($PSCompletions.path.completions)/$completion/language"
            foreach ($lang in $content_config.language) {
                $path_lang = "$($PSCompletions.path.completions)/$completion/language/$lang.json"
                if (!(Test-Path $path_lang)) {
                    download_file "completions/$completion/language/$lang.json" $path_lang $PSCompletions.urls
                }
            }
            if ($content_config.hooks -ne $null) {
                $path_hooks = "$($PSCompletions.path.completions)/$completion/hooks.ps1"
                if (!(Test-Path $path_hooks)) {
                    download_file "completions/$completion/hooks.ps1" $path_hooks $PSCompletions.urls
                }
            }
            if ($PSCompletions.config.comp_config[$completion].language) {
                $config_language = $PSCompletions.config.comp_config.$completion.language
            }
            else {
                $config_language = $null
            }
            if ($config_language) {
                $language = if ($config_language -in $content_config.language) { $config_language }else { $content_config.language[0] }
            }
            else {
                $language = if ($PSCompletions.language -in $content_config.language) { $PSCompletions.language }else { $content_config.language[0] }
            }
            $language
        }

        # XXX: 如果存在大量补全,考虑使用 runspace, 参考 $PSCompletions.handle_data_by_runspace
        $_completions = @{}
        $_completions_data = @{}
        $time = (Get-Date).AddMonths(-6)
        $filter = (Get-ChildItem $PSCompletions.path.order).Where({ $_.LastWriteTime -gt $time })
        foreach ($_ in $filter) {
            $root = $_.BaseName
            if ($root -in $PSCompletions.data.list) {
                try {
                    $language = get_language $root
                }
                catch {
                    continue
                }
                $path_language = "$($PSCompletions.path.completions)/$root/language/$language.json"
                if (Test-Path $path_language) {
                    $_completions.$root = get_raw_content $path_language | ConvertFrom-Json -AsHashtable
                    $_completions_data.$root = getCompletions
                }
            }
        }
        return @{
            completions      = $_completions
            completions_data = $_completions_data
        }
    } -ArgumentList $PSCompletions
}

Add-Member -InputObject $PSCompletions -MemberType ScriptMethod order_job {
    param([string]$history_path, [string]$root, [string]$path_order)
    $PSCompletions.order."$($root)_job" = Start-ThreadJob -ScriptBlock {
        param($PScompletions, [string]$path_history, [string]$root, [string]$path_order)

        $order_dir = $PSCompletions.path.order
        if (!(Test-Path $order_dir)) {
            New-Item -ItemType Directory -Path $order_dir -Force | Out-Null
        }

        $order = [ordered]@{}
        $index = 0
        foreach ($_ in Get-Content $path_history -Encoding utf8 -ErrorAction SilentlyContinue) {
            $alias = $PSCompletions.data.alias.$root
            if (!$alias) {
                $alias = @($root)
            }
            foreach ($a in $alias) {
                if ($_ -match "^[^\S\n]*$a\s+.+") {
                    $_ = $_ -replace '^\w+\s+', ''
                    $input_arr = @()
                    $matches = [regex]::Matches($_, "(?:`"[^`"]*`"|'[^']*'|\S)+")
                    foreach ($match in $matches) { $input_arr += $match.Value }
                    $index += $input_arr.Count
                    $i = 0
                    foreach ($completion in $input_arr) {
                        $order.$completion = $index + $i
                        $i--
                    }
                    break
                }
            }
        }

        $index = 0
        $result = [ordered]@{}
        $sorted = $order.Keys | Sort-Object { $order.$_ } -CaseSensitive
        foreach ($_ in $sorted) {
            $index++
            $result.$_ = $index
        }

        $old = Get-Content -Raw $path_order -ErrorAction SilentlyContinue | ConvertFrom-Json | ConvertTo-Json -Depth 100 -Compress
        $new = $result | ConvertTo-Json -Depth 100 -Compress
        if ($new -ne $old) {
            $new | Out-File $path_order -Force -Encoding utf8
        }

        return $result
    } -ArgumentList $PScompletions, $history_path, $root, $path_order
}