core/init.ps1
using namespace System.Management.Automation $_ = Split-Path $PSScriptRoot -Parent New-Variable -Name PSCompletions -Value @{ version = '5.2.1' path = @{ root = $_ completions = Join-Path $_ 'completions' core = Join-Path $_ 'core' data = Join-Path $_ 'data.json' temp = Join-Path $_ 'temp' order = Join-Path $_ 'temp\order' completions_json = Join-Path $_ 'temp\completions.json' update = Join-Path $_ 'temp\update.txt' change = Join-Path $_ 'temp\change.txt' } order = [ordered]@{} root_cmd = '' completions_data = @{} guid = [guid]::NewGuid().Guid language = $PSUICulture encoding = [console]::OutputEncoding separator = [System.IO.Path]::DirectorySeparatorChar wc = New-Object System.Net.WebClient menu = @{ const = @{ symbol_item = @('SpaceTab', 'WriteSpaceTab', 'OptionTab') line_item = @('horizontal', 'vertical', 'top_left', 'bottom_left', 'top_right', 'bottom_right') color_item = @('item_text', 'item_back', 'selected_text', 'selected_back', 'filter_text', 'filter_back', 'border_text', 'border_back', 'status_text', 'status_back', 'tip_text', 'tip_back') color_value = @('White', 'Black', 'Gray', 'DarkGray', 'Red', 'DarkRed', 'Green', 'DarkGreen', 'Blue', 'DarkBlue', 'Cyan', 'DarkCyan', 'Yellow', 'DarkYellow', 'Magenta', 'DarkMagenta') config_item = @( 'trigger_key', 'between_item_and_symbol', 'status_symbol', 'filter_symbol', 'enable_menu', 'enable_menu_enhance', 'enable_tip', 'enable_tip_when_enhance', 'enable_completions_sort', 'enable_tip_follow_cursor', 'enable_list_follow_cursor', 'enable_tip_cover_buffer', 'enable_list_cover_buffer', 'enable_list_loop', 'enable_selection_with_margin', 'enable_enter_when_single', 'enable_prefix_match_in_filter', 'list_min_width', 'list_max_count_when_above', 'list_max_count_when_below', 'width_from_menu_left_to_item', 'width_from_menu_right_to_item', 'height_from_menu_bottom_to_cursor_when_above' ) } } default_config = [ordered]@{ # config url = '' language = $PSUICulture enable_completions_update = 1 enable_module_update = 1 disable_cache = 0 function_name = 'PSCompletions' # menu symbol SpaceTab = '»' WriteSpaceTab = '!' OptionTab = '?' # menu line horizontal = [string][char]9552 # ═ vertical = [string][char]9553 # ║ top_left = [string][char]9556 # ╔ bottom_left = [string][char]9562 # ╚ top_right = [string][char]9559 # ╗ bottom_right = [string][char]9565 # ╝ # menu color item_text = 'Blue' item_back = 'Black' selected_text = 'white' selected_back = 'DarkGray' filter_text = 'Yellow' filter_back = 'Black' border_text = 'DarkGray' border_back = 'Black' status_text = 'Blue' status_back = 'Black' tip_text = 'Cyan' tip_back = 'Black' # menu config trigger_key = 'Tab' between_item_and_symbol = ' ' status_symbol = '/' filter_symbol = '[]' enable_menu = 1 enable_menu_enhance = 1 enable_tip = 1 enable_tip_when_enhance = 1 enable_completions_sort = 1 enable_tip_follow_cursor = 1 enable_list_follow_cursor = 1 enable_tip_cover_buffer = 1 enable_list_cover_buffer = 0 enable_list_loop = 1 enable_selection_with_margin = 1 enable_enter_when_single = 0 enable_prefix_match_in_filter = 0 list_min_width = 10 list_max_count_when_above = -1 list_max_count_when_below = -1 width_from_menu_left_to_item = 0 width_from_menu_right_to_item = 0 height_from_menu_bottom_to_cursor_when_above = 0 } # 每个补全都默认带有的配置项 default_completion_item = @('language', 'enable_tip') config_item = @('url', 'language', 'enable_completions_update', 'enable_module_update', 'disable_cache', 'function_name') } -Option ReadOnly if ($IsWindows -or $PSEdition -eq 'Desktop') { # Windows... . $PSScriptRoot\completion\win.ps1 . $PSScriptRoot\menu\win.ps1 } else { # WSL/Unix... . $PSScriptRoot\completion\unix.ps1 } . $PSScriptRoot\utils\$PSEdition.ps1 Add-Member -InputObject $PSCompletions -MemberType ScriptMethod return_completion { param([string]$name, $tip = ' ', [array]$symbols) @{ ListItemText = $name CompletionText = $name ToolTip = $tip symbols = $symbols } } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod get_completion { $guid = $PSCompletions.guid function getCompletions { $obj = @{} $special_options = @{ WriteSpaceTab = @() WriteSpaceTab_and_SpaceTab = @() } function parseJson($cmds, $obj, $cmdO, [switch]$isOption) { if ($obj[$cmdO].$guid -eq $null) { $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 += $PSCompletions.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 ($PSCompletions.completions[$root].root) { parseJson $PSCompletions.completions[$root].root $obj 'root' } if ($PSCompletions.completions[$root].options) { parseJson $PSCompletions.completions[$root].options $obj 'rootOptions' -isOption } if ($PSCompletions.completions[$root].common_options) { parseJson $PSCompletions.completions[$root].common_options $obj 'commonOptions' -isOption } $PSCompletions.completions_data."$($root)_WriteSpaceTab" = $special_options.WriteSpaceTab | Select-Object -Unique $PSCompletions.completions_data."$($root)_WriteSpaceTab_and_SpaceTab" = $special_options.WriteSpaceTab_and_SpaceTab | Select-Object -Unique return $obj } function handleCompletions { param($completions) return $completions } function filterCompletions { function readObject { param ( [hashtable]$obj, # 要读取的对象 [string[]]$attrs, # 属性名称数组 $return = $null # 如果属性不存在,返回的默认值 ) $v = $obj foreach ($attr in $attrs) { if ($v -is [hashtable] -and $v.ContainsKey($attr)) { $v = $v[$attr] } else { return $return } } return $v } function filterInput { param( [array]$input_arr ) $need_skip = $false $filter_input_arr = @() $last_item = $input_arr[-1] $pre_cmd = '' $commonOptions = $PSCompletions.completions_data."$($root)_common_options" $WriteSpaceTab = $PSCompletions.completions_data."$($root)_WriteSpaceTab" $WriteSpaceTab_and_SpaceTab = $PSCompletions.completions_data."$($root)_WriteSpaceTab_and_SpaceTab" foreach ($_ in $input_arr) { if ($need_skip) { if ($_ -like '-*') { if ($_ -eq $last_item) { $filter_input_arr += $_ } } else { $need_skip = $false continue } } else { $pad = if ($_ -in $commonOptions) { '' } else { $pre_cmd } if ($_ -like '-*') { if ("$pad $_" -in $WriteSpaceTab) { if ("$pad $_" -in $WriteSpaceTab_and_SpaceTab) { # 如果选项是最后一个 if ($_ -eq $last_item) { $filter_input_arr += $_ } else { $need_skip = $true } } else { # 需要用户输入,但是没有候选项 $need_skip = $true } } } else { $filter_input_arr += $_ # 记录上一个子命令 $pre_cmd = $_ } } } return $filter_input_arr } $filter_list = @() $PSCompletions.filter_input_arr = @() $PSCompletions.filter_input_str = '' # 如果不只是输入了根命令 if ($input_arr) { # 这里过滤后只存在两种情况 # 1. 没有选项 # 2. 最后一个是选项 if ($space_tab) { $filter_input_arr = [array](filterInput $input_arr) } else { $handle_input_arr = if ($input_arr.Count -eq 1) { , @() }else { $input_arr[0..($input_arr.Count - 2)] } $filter_input_arr = [array](filterInput $handle_input_arr) } if ($filter_input_arr.Count) { $PSCompletions.filter_input_arr = $filter_input_arr } else { if ($completions.root.$guid) { $filter_list += $completions.root.$guid } if ($completions.rootOptions.$guid) { $filter_list += $completions.rootOptions.$guid } if ($completions.commonOptions.$guid) { $filter_list += $completions.commonOptions.$guid } return $filter_list } $PSCompletions.filter_input_str = $filter_input_str = $filter_input_arr -join ' ' $last_item = $filter_input_arr[-1] $no_options = $true $all_options = $true foreach ($c in $filter_input_arr) { if ($c -like "-*") { $no_options = $false } else { $all_options = $false } } if ($last_item -like '-*') { if ($all_options) { # 如果全是选项,只可能是根选项或通用选项 $data = $PSCompletions.completions_data.$root.commonOptions.$last_item.$guid if (!$data.Count) { $data = $PSCompletions.completions_data.$root.rootOptions.$last_item.$guid } if ($data.Count) { $filter_list = $data } else { $data = readObject $completions.root ($filter_input_arr + $guid) (, @()) if ($data.Count) { $filter_list += $data } else { if ($completions.root.$guid) { $filter_list += $completions.root.$guid } if ($completions.rootOptions.$guid) { $filter_list += $completions.rootOptions.$guid } if ($completions.commonOptions.$guid) { $filter_list += $completions.commonOptions.$guid } } } } else { if ($last_item -in $PSCompletions.completions_data."$($root)_common_options") { $filter_list = $PSCompletions.completions_data.$root.commonOptions.$last_item.$guid } else { $filter_list = readObject $completions.root ($filter_input_arr + $guid) (, @()) } } } else { $filter_list += readObject $completions.root ($filter_input_arr + $guid) (, @()) if ($completions.commonOptions.$guid) { $filter_list += $completions.commonOptions.$guid } } return $filter_list } else { # 如果 $input_arr 没有值,即只输入了根命令进行补全 if ($completions.root.$guid) { $filter_list += $completions.root.$guid } if ($completions.rootOptions.$guid) { $filter_list += $completions.rootOptions.$guid } if ($completions.commonOptions.$guid) { $filter_list += $completions.commonOptions.$guid } return $filter_list } } if ($PSCompletions.job.State -eq 'Completed') { if (!$PSCompletions._has_add_completion) { $_data = Receive-Job $PSCompletions.job $PSCompletions.completions = $_data.completions $PSCompletions.completions_data = $_data.completions_data } Remove-Job $PSCompletions.job $PSCompletions.job = $null } if ($PSCompletions.config.disable_cache -eq 1) { $PSCompletions.completions.$root = $null $PSCompletions.completions_data.$root = $null } if (!$PSCompletions.completions[$root]) { $language = $PSCompletions.get_language($root) $PSCompletions.completions.$root = $PSCompletions.ConvertFrom_JsonToHashtable($PSCompletions.get_raw_content("$($PSCompletions.path.completions)/$root/language/$language.json")) } $input_arr = [array]$input_arr $PSCompletions.input_arr = $input_arr # 使用 hooks 覆盖默认的函数,实现一些特殊的需求,比如一些补全的动态加载 if ($PSCompletions.config.comp_config[$root].enable_hooks -eq 1) { . "$($PSCompletions.path.completions)/$root/hooks.ps1" } if (!$PSCompletions.completions_data[$root]) { $PSCompletions.completions_data.$root = getCompletions $PSCompletions.completions_data."$($root)_common_options" = $PSCompletions.completions_data.$root.commonOptions.$guid | ForEach-Object { $_.CompletionText } } $completions = $PSCompletions.completions_data.$root $filter_list = [array](filterCompletions) $filter_list = [array](handleCompletions $filter_list) if ($space_tab) { $filter_list = $filter_list.Where({ $_.CompletionText -notlike "-*" -or $_.CompletionText -notin $input_arr }) } else { $filter_list = $filter_list.Where({ $_.CompletionText -like "$([WildcardPattern]::Escape($input_arr[-1]))*" }) } if ($PSCompletions.config.enable_completions_sort -eq 1) { $path_order = "$($PSCompletions.path.order)/$root.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) { try { $PSCompletions.order.$root = $PSCompletions.ConvertFrom_JsonToHashtable($PSCompletions.get_raw_content($path_order)) } catch { $PSCompletions.order.$root = $null } } else { $PSCompletions.order.$root = $null } } $order = $PSCompletions.order.$root if ($order) { $PSCompletions._i = 0 # 这里使用 $PSCompletions._i 而非 $i 是因为在 Sort-Object 中,普通的 $i 无法累计 $filter_list = $filter_list | Sort-Object { $PSCompletions._i -- # 不能使用 $order.($_.CompletionText),它可能获取到对象中的 OverloadDefinitions $o = $order[$_.CompletionText] if ($o) { $o }else { $PSCompletions._i } } -Descending -CaseSensitive } $PSCompletions.order_job((Get-PSReadLineOption).HistorySavePath, $root, $path_order) } $filter_list = [array]$filter_list $return = @() if ($space_tab) { foreach ($item in $filter_list) { if ($item -ne $null) { $has_alias = $false if ($item.alias) { foreach ($a in $item.alias) { if ($a -in $PSCompletions.input_arr) { $has_alias = $true break } } } if (!$has_alias) { $padSymbols = foreach ($c in $item.symbols) { $PSCompletions.config.$c } $padSymbols = if ($padSymbols) { "$($PSCompletions.config.between_item_and_symbol)$($padSymbols -join '')" }else { '' } $return += @{ ListItemText = $item.ListItemText + $padSymbols CompletionText = $item.CompletionText ToolTip = $item.ToolTip } } } } return $return } else { foreach ($item in $filter_list) { if ($item -ne $null) { $padSymbols = foreach ($c in $item.symbols) { $PSCompletions.config.$c } $padSymbols = if ($padSymbols) { "$($PSCompletions.config.between_item_and_symbol)$($padSymbols -join '')" }else { '' } $return += @{ ListItemText = $item.ListItemText + $padSymbols CompletionText = $item.CompletionText ToolTip = $item.ToolTip } } } return $return } } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod handle_data_by_runspace { param( [array]$list, # 需要处理的数据 # 处理逻辑,可以获取三个参数 # 1. $arr: 分组后的部分需要处理的数据 # 2. $PSCompletions # 3. $Host.UI [scriptblock]$handler, # 处理结果,可以获取一个参数 # 1. $results: $handler 脚本块返回的结果 [scriptblock]$handleResult ) $runspaces = @() $runspacePool = [runspacefactory]::CreateRunspacePool(1, [Environment]::ProcessorCount) $runspacePool.Open() foreach ($arr in $PSCompletions.split_array($list, [Environment]::ProcessorCount, $true)) { $runspace = [powershell]::Create().AddScript($handler).AddArgument($arr).AddArgument($PSCompletions).AddArgument($Host.UI) $runspace.RunspacePool = $runspacePool $runspaces += @{ Runspace = $runspace; Job = $runspace.BeginInvoke() } } $return = @() foreach ($rs in $runspaces) { $results = $rs.Runspace.EndInvoke($rs.Job) $rs.Runspace.Dispose() $return += & $handleResult $results } $runspacePool.Close() $runspacePool.Dispose() return $return } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod split_array { <# .Synopsis 分割数组 .Description 三个参数: $array: 数组 $count: 每个数组中的元素个数 $by_count: 如果此值为 $true, 则 $count 为分割的数组个数,否则为每个数组中的元素个数 #> param ([array]$array, [int]$count, [bool]$by_count) if ($by_count) { $ChunkSize = [math]::Ceiling($array.Length / $count) } else { $ChunkSize = $count } $chunks = for ($i = 0; $i -lt $array.Length; $i += $ChunkSize) { , ($array[$i..([math]::Min($i + $ChunkSize - 1, $array.Length - 1))]) } $chunks } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod ensure_dir { param([string]$path) if (!(Test-Path $path)) { New-Item -ItemType Directory $path > $null } } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod get_language { param ([string]$completion) $path_config = "$($PSCompletions.path.completions)/$completion/config.json" $content_config = $PSCompletions.get_raw_content($path_config) | ConvertFrom-Json if (!$content_config.language) { $PSCompletions.download_file("$($PSCompletions.url)/completions/$completion/config.json", $path_config) $content_config = $PSCompletions.get_raw_content($path_config) | ConvertFrom-Json } 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 } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod get_content { param ([string]$path) $res = (Get-Content $path -Encoding utf8 -ErrorAction SilentlyContinue).Where({ $_ -ne '' }) if ($res) { return $res } , @() } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod 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 } '' } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod 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) { $PSCompletions.replace_content($data) }else { return $data } } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod write_with_color { param([string]$str) $color_list = @() $str = $str -replace "`n", 'n&&_n_n&&' $str_list = foreach ($_ in ($str -split '(<\@[^>]+>.*?(?=<\@|$))').Where({ $_ -ne '' })) { if ($_ -match '<\@([\s\w]+)>(.*)') { ($matches[2] -replace 'n&&_n_n&&', "`n") -replace '^<\@>', '' $color = $matches[1] -split ' ' $color_list += @{ color = $color[0] bgcolor = $color[1] } } else { ($_ -replace 'n&&_n_n&&', "`n") -replace '^<\@>', '' $color_list += @{} } } $str_list = [array]$str_list for ($i = 0; $i -lt $str_list.Count; $i++) { $color = $color_list[$i].color $bgcolor = $color_list[$i].bgcolor if ($color) { if ($bgcolor) { Write-Host $str_list[$i] -f $color -b $bgcolor -NoNewline } else { Write-Host $str_list[$i] -f $color -NoNewline } } else { Write-Host $str_list[$i] -NoNewline } } Write-Host '' } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod show_with_less { param ( $str_list, [string]$color = 'Green', [int]$show_line = [System.Console]::WindowHeight - 7 ) if ($str_list -is [string]) { $str_list = $str_list -split "`n" } $i = 0 $need_less = [System.Console]::WindowHeight -lt ($str_list.Count + 2) if ($need_less) { $lines = $str_list.Count - $show_line $PSCompletions.write_with_color($PSCompletions.replace_content($PSCompletions.info.less_tip)) while ($i -lt $str_list.Count -and $i -lt $show_line) { Write-Host $str_list[$i] -f $color $i++ } while ($i -lt $str_list.Count) { $keyCode = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode if ($keyCode -ne 13) { break } Write-Host $str_list[$i] -f $color $i++ } $end = if ($i -lt $str_list.Count) { $false }else { $true } if ($end) { Write-Host ' ' Write-Host '(End)' -f Black -b White -NoNewline while ($end) { $keyCode = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode if ($keyCode -ne 13) { Write-Host ' ' break } } } } else { foreach ($_ in $str_list) { Write-Host $_ -f $color } } } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod show_with_less_table { param ( [array]$str_list, [array]$header = @('key', 'value', 5), [scriptblock]$do = {}, [int]$show_line = [System.Console]::WindowHeight - 7 ) $str_list = @( @{ content = "`n{0,-$($header[2] + 3)} {1}" -f $header[0], $header[1] color = 'Cyan' }, @{ content = "{0,-$($header[2] + 3)} {1}" -f ('-' * $header[0].Length), ('-' * $header[1].Length) color = 'Cyan' } ) + $str_list $i = 0 $need_less = [System.Console]::WindowHeight -lt ($str_list.Count + 2) if ($need_less) { $lines = $str_list.Count - $show_line $PSCompletions.write_with_color($PSCompletions.replace_content($PSCompletions.info.less_tip)) & $do while ($i -lt $str_list.Count -and $i -lt $show_line) { if ($str_list[$i].bgColor) { Write-Host $str_list[$i].content -f $str_list[$i].color -b $str_list[$i].bgColor } else { Write-Host $str_list[$i].content -f $str_list[$i].color } $i++ } while ($i -lt $str_list.Count) { $keyCode = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode if ($keyCode -ne 13) { break } if ($str_list[$i].bgColor) { Write-Host $str_list[$i].content -f $str_list[$i].color -b $str_list[$i].bgColor } else { Write-Host $str_list[$i].content -f $str_list[$i].color } $i++ } $end = if ($i -lt $str_list.Count) { $false }else { $true } if ($end) { Write-Host ' ' Write-Host '(End)' -f Black -b White -NoNewline while ($end) { $keyCode = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode if ($keyCode -ne 13) { Write-Host ' ' break } } } } else { & $do foreach ($_ in $str_list) { if ($_.bgColor) { Write-Host $_.content -f $_.color -b $_.bgColor[2] } else { Write-Host $_.content -f $_.color } } } } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod download_list { $PSCompletions.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 = ($PSCompletions.get_raw_content($PSCompletions.path.completions_json) | ConvertFrom-Json).list try { $content = (Invoke-WebRequest -Uri "$($PSCompletions.url)/completions.json").Content | ConvertFrom-Json $remote_list = $content.list $diff = Compare-Object $remote_list $current_list -PassThru if ($diff) { if ($PSCompletions.is_update_init) { $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 } return $remote_list } catch { $PSCompletions.list = $current_list $PSCompletions._invalid_url = "$($PSCompletions.url)/completions.json" return $false } } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod download_file { param([string]$url, [string]$file) try { $PSCompletions.wc.DownloadFile($url, $file) } catch { if ($PSCompletions.info) { $download_info = @{ url = $url file = $file } $PSCompletions.write_with_color($PSCompletions.replace_content($PSCompletions.info.err.download_file)) } else { Write-Host "File ($(Split-Path $url -Leaf)) download failed, please check your network connection or try again later." -ForegroundColor Red Write-Host "File download Url: $url" -ForegroundColor Red Write-Host "File save path: $file" -ForegroundColor Red Write-Host "If you are sure that it is not a network problem, please submit an issue`n" -ForegroundColor Red } throw $_ } } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod add_completion { param ( [string]$completion, [bool]$log = $true, [bool]$is_update = $true ) $PSCompletions._has_add_completion = $log -and $true $PSCompletions.completions_data[$completion] = $null $PSCompletions.completions[$completion] = $null $url = "$($PSCompletions.url)/completions/$completion" $is_update = (Test-Path "$($PSCompletions.path.completions)/$completion") -and $is_update $completion_dir = Join-Path $PSCompletions.path.completions $completion $language_dir = Join-Path $completion_dir 'language' $PSCompletions.ensure_dir($PSCompletions.path.completions) $PSCompletions.ensure_dir($completion_dir) $PSCompletions.ensure_dir($language_dir) $download_info = @{ url = "$url/config.json" file = Join-Path $completion_dir 'config.json' } $PSCompletions.download_file($download_info.url, $download_info.file) $config = $PSCompletions.get_raw_content("$completion_dir/config.json") | ConvertFrom-Json $config | ConvertTo-Json -Compress -Depth 100 | Out-File $download_info.file -Encoding utf8 -Force $files = @( @{ Uri = "$url/guid.txt" OutFile = Join-Path $completion_dir 'guid.txt' } ) foreach ($_ in $config.language) { $files += @{ Uri = "$url/language/$_.json" OutFile = Join-Path $language_dir "$_.json" } } if ($config.hooks) { $files += @{ Uri = "$url/hooks.ps1" OutFile = Join-Path $completion_dir 'hooks.ps1' } } foreach ($file in $files) { $download_info = @{ url = $file.Uri file = $file.OutFile } try { $PSCompletions.wc.DownloadFile($download_info.url, $download_info.file) if ($download_info.file -match '\.json$') { $PSCompletions.get_raw_content($download_info.file) | ConvertFrom-Json | ConvertTo-Json -Compress -Depth 100 | Out-File $download_info.file -Encoding utf8 -Force } } catch { Remove-Item $completion_dir -Force -Recurse -ErrorAction SilentlyContinue throw } } # 显示下载信息 $download = if ($is_update) { $PSCompletions.info.update.doing }else { $PSCompletions.info.add.doing } if ($log) { $PSCompletions.write_with_color("`n" + $PSCompletions.replace_content($download)) } $done = if ($is_update) { $PSCompletions.info.update.done }else { $PSCompletions.info.add.done } if ($completion -notin $PSCompletions.data.list) { $PSCompletions.data.list += $completion $PSCompletions._need_update_data = $true } if (!$PSCompletions.data.alias[$completion]) { $PSCompletions.data.alias.$completion = @() } $PSCompletions._alias_conflict = $false $conflict_alias_list = @() if ($config.alias) { foreach ($a in $config.alias) { if ($a -notin $PSCompletions.data.alias.$completion) { $PSCompletions.data.alias.$completion += $a if ($PSCompletions.data.aliasMap[$a]) { $PSCompletions._alias_conflict = $true $conflict_alias_list += $a } else { $PSCompletions.data.aliasMap.$a = $completion } $PSCompletions._need_update_data = $true } } } else { if ($completion -notin $PSCompletions.data.alias.$completion) { $PSCompletions.data.alias.$completion += $completion if ($PSCompletions.data.aliasMap[$completion]) { $PSCompletions._alias_conflict = $true $conflict_alias_list += $completion } else { $PSCompletions.data.aliasMap.$completion = $completion } $PSCompletions._need_update_data = $true } } if ($PSCompletions._alias_conflict) { $PSCompletions.write_with_color($PSCompletions.replace_content($PSCompletions.info.err.alias_conflict)) } $language = $PSCompletions.get_language($completion) $json = $PSCompletions.ConvertFrom_JsonToHashtable($PSCompletions.get_raw_content("$completion_dir/language/$language.json")) if (!$PSCompletions.completions) { $PSCompletions.completions = @{} } $PSCompletions.completions.$completion = $json if ($log) { $PSCompletions.write_with_color($PSCompletions.replace_content($done)) } if ($json.config) { if (!$PSCompletions.config.comp_config[$completion]) { $PSCompletions.config.comp_config.$completion = @{} } foreach ($_ in $json.config) { if (!($PSCompletions.config.comp_config.$completion.$($_.name))) { $PSCompletions.config.comp_config.$completion.$($_.name) = $_.value $PSCompletions._need_update_data = $true } } } } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod new_data { $data = @{ list = @() alias = @{} aliasMap = @{} config = $PSCompletions.default_config } $data.config.comp_config = @{} foreach ($_ in Get-ChildItem -Path $PSCompletions.path.completions -Directory) { $name = $_.Name $data.list += $name $data.alias.$name = @() $path_config = Join-Path $_.FullName 'config.json' if (!(Test-Path $path_config)) { continue } $config = $PSCompletions.get_raw_content($path_config) | ConvertFrom-Json if ($config.alias) { foreach ($a in $config.alias) { $data.alias.$name += $a $data.aliasMap.$a = $name } } else { $data.alias.$name += $name $data.aliasMap.$name = $name } $language = if ($PSCompletions.language -eq 'zh-CN') { 'zh-CN' }else { 'en-US' } $json = $PSCompletions.ConvertFrom_JsonToHashtable($PSCompletions.get_raw_content("$($_.FullName)/language/$language.json")) $data.config.comp_config.$name = @{} foreach ($_ in $json.config) { $data.config.comp_config.$name.$($_.name) = $_.value } } $data | ConvertTo-Json -Depth 100 -Compress | Out-File $PSCompletions.path.data -Force -Encoding utf8 $PSCompletions.data = $data $null = $PSCompletions.download_list() } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod init_data { $PSCompletions.completions = @{} if ((Test-Path $PSCompletions.path.data) -and !$PSCompletions.is_first_init) { $PSCompletions.data = $PSCompletions.ConvertFrom_JsonToHashtable($PSCompletions.get_raw_content($PSCompletions.path.data)) } else { $PSCompletions.new_data() } $PSCompletions.config = $PSCompletions.data.config $PSCompletions.language = $PSCompletions.config.language if ($PSCompletions.config.url) { $PSCompletions.url = $PSCompletions.config.url } else { if ($PSCompletions.language -eq 'zh-CN') { $PSCompletions.url = 'https://gitee.com/abgox/PSCompletions/raw/main' } else { $PSCompletions.url = 'https://raw.githubusercontent.com/abgox/PSCompletions/main' } } $PSCompletions.list = ($PSCompletions.get_raw_content($PSCompletions.path.completions_json) | ConvertFrom-Json).list $PSCompletions.update = $PSCompletions.get_content($PSCompletions.path.update) if ('psc' -notin $PSCompletions.data.list -or $PSCompletions.is_init) { $PSCompletions.add_completion('psc', $false, $false) $PSCompletions.data | ConvertTo-Json -Depth 100 -Compress | Out-File $PSCompletions.path.data -Force -Encoding utf8 $PSCompletions.is_init = $null } if ($PSCompletions.completions.psc.info) { $PSCompletions.info = $PSCompletions.completions.psc.info } else { $language = if ($PSCompletions.language -eq 'zh-CN') { 'zh-CN' }else { 'en-US' } $PSCompletions.info = $PSCompletions.ConvertFrom_JsonToHashtable($PSCompletions.get_raw_content("$($PSCompletions.path.completions)/psc/language/$language.json")).info } } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod get_length { param([string]$str) $Host.UI.RawUI.NewBufferCellArray($str, $Host.UI.RawUI.BackgroundColor, $Host.UI.RawUI.BackgroundColor).LongLength } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod show_powershell_menu { param([array]$filter_list) if ($Host.UI.RawUI.BufferSize.Height -lt 5) { [Microsoft.PowerShell.PSConsoleReadLine]::UndoAll() [Microsoft.PowerShell.PSConsoleReadLine]::Insert($PSCompletions.info.min_area) '' return } $json = $PSCompletions.completions.$($PSCompletions.root_cmd) $info = $json.info # is_show_tip $enable_tip = $PSCompletions.config.comp_config.$($PSCompletions.root_cmd).enable_tip if ($enable_tip -ne $null) { $PSCompletions.menu.is_show_tip = $enable_tip -eq 1 } else { $PSCompletions.menu.is_show_tip = $PSCompletions.config.enable_tip -eq 1 } if ($PSCompletions.menu.is_show_tip) { foreach ($_ in $filter_list) { if ($_.ToolTip -ne $null) { $tip = $PSCompletions.replace_content($_.ToolTip) } else { $tip = ' ' } $padSymbols = foreach ($c in $_.symbols) { $PSCompletions.config.$c } $padSymbols = if ($padSymbols) { "$($PSCompletions.config.between_item_and_symbol)$($padSymbols -join '')" }else { '' } [CompletionResult]::new($_.CompletionText, ($_.ListItemText + $padSymbols), 'ParameterValue', $tip) } } else { foreach ($_ in $filter_list) { $padSymbols = foreach ($c in $_.symbols) { $PSCompletions.config.$c } $padSymbols = if ($padSymbols) { "$($PSCompletions.config.between_item_and_symbol)$($padSymbols -join '')" }else { '' } [CompletionResult]::new($_.CompletionText, ($_.ListItemText + $padSymbols), 'ParameterValue', ' ') } } } Add-Member -InputObject $PSCompletions -MemberType ScriptMethod argc_completions { param( [array]$completions, # The list of completions. [bool]$isShowTip = $PSCompletions.config.enable_tip_when_enhance # Set whether to display the tooltip or not. ) $PSCompletions.menu.is_show_tip = $isShowTip foreach ($_ in $completions) { Register-ArgumentCompleter -Native -CommandName $_ -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) $words = @($commandAst.CommandElements | Where { $_.Extent.StartOffset -lt $cursorPosition } | ForEach-Object { $word = $_.ToString() if ($word.Length -gt 2) { if (($word.StartsWith('"') -and $word.EndsWith('"')) -or ($word.StartsWith("'") -and $word.EndsWith("'"))) { $word = $word.Substring(1, $word.Length - 2) } } return $word }) $emptyS = '' if ($PSVersionTable.PSVersion.Major -eq 5) { $emptyS = '""' } $lastElemIndex = -1 if ($words.Count -lt $commandAst.CommandElements.Count) { $lastElemIndex = $words.Count - 1 } if ($commandAst.CommandElements[$lastElemIndex].Extent.EndOffset -lt $cursorPosition) { $words += $emptyS } @((argc --argc-compgen powershell $emptyS $words) -split "`n") | ForEach-Object { $parts = ($_ -split "`t") if ($PSCompletions.menu.is_show_tip) { $tip = if ($parts[3] -eq '') { ' ' }else { $parts[3] } [CompletionResult]::new($parts[0], $parts[0], [CompletionResultType]::ParameterValue, $tip) } else { [CompletionResult]::new($parts[0], $parts[0], [CompletionResultType]::ParameterValue, ' ') } } } } } if (!(Test-Path $PSCompletions.path.temp)) { $PSCompletions.data = $PSCompletions.ConvertFrom_JsonToHashtable($PSCompletions.get_raw_content($PSCompletions.path.data)) $PSCompletions.is_init = $true if ($PSCompletions.data.config) { $PSCompletions.is_update_init = $true } else { $PSCompletions.is_first_init = $true $PSCompletions.data = $null } $PSCompletions.ensure_dir($PSCompletions.path.temp) $PSCompletions.ensure_dir($PSCompletions.path.order) Add-Member -InputObject $PSCompletions -MemberType ScriptMethod move_old_version { $version = (Get-ChildItem (Split-Path $PSCompletions.path.root -Parent) -ErrorAction SilentlyContinue).Name | Sort-Object { [Version]$_ } -ErrorAction SilentlyContinue | Where-Object { $_ -match '^\d+\.\d.*' } if ($version -is [array]) { $old_version = $version[-2] if ($old_version -match '^\d+\.\d.*' -and $old_version -ge '4') { $old_version_dir = Join-Path (Split-Path $PSCompletions.path.root -Parent) $old_version $PSCompletions.ensure_dir($PSCompletions.path.completions) if (Test-Path "$old_version_dir/data.json") { foreach ($_ in Get-ChildItem "$old_version_dir/completions" -Directory -ErrorAction SilentlyContinue) { if ($_.Name -ne 'psc') { Move-Item $_.FullName $PSCompletions.path.completions -Force -ErrorAction SilentlyContinue } } Move-Item "$old_version_dir/data.json" $PSCompletions.path.data -Force -ErrorAction SilentlyContinue } else { $data = @{ list = @() alias = @{} aliasMap = @{} config = $PSCompletions.default_config } $data.config.comp_config = @{} foreach ($_ in Get-ChildItem "$old_version_dir/completions" -Directory -ErrorAction SilentlyContinue) { $name = $_.Name $data.list += $name $data.alias.$name = @() $path_alias = Join-Path $_.FullName 'alias.txt' if (Test-Path $path_alias) { $alias_list = $PSCompletions.get_content($path_alias) foreach ($a in $alias_list) { $data.alias.$name += $a $data.aliasMap.$a = $name } } else { $path_config = Join-Path $_.FullName 'config.json' $config = $PSCompletions.get_raw_content($path_config) | ConvertFrom-Json if ($config.alias) { foreach ($a in $config.alias) { $data.alias.$name += $a $data.aliasMap.$a = $name } } else { $data.alias.$name += $name $data.aliasMap.$name = $name } } if ($_.Name -ne 'psc') { Move-Item $_.FullName $PSCompletions.path.completions -Force -ErrorAction SilentlyContinue } } $data | ConvertTo-Json -Depth 100 -Compress | Out-File $PSCompletions.path.data -Force -Encoding utf8 } Move-Item "$old_version_dir/temp/update.txt" $PSCompletions.path.update -Force -ErrorAction SilentlyContinue Move-Item "$old_version_dir/temp/change.txt" $PSCompletions.path.change -Force -ErrorAction SilentlyContinue Move-Item "$old_version_dir/temp/completions.json" $PSCompletions.path.completions_json -Force -ErrorAction SilentlyContinue } } else { if ($PSUICulture -eq 'zh-CN') { $language = 'zh-CN' $PSCompletions.url = 'https://gitee.com/abgox/PSCompletions/raw/main' } else { $language = 'en-US' $PSCompletions.url = 'https://raw.githubusercontent.com/abgox/PSCompletions/main' } $PSCompletions.ensure_dir("$($PSCompletions.path.completions)/psc/language") $file_list = @('language/zh-CN.json', 'language/en-US.json', 'config.json', 'guid.txt', 'hooks.ps1') foreach ($_ in $file_list) { $outFile = "$($PSCompletions.path.completions)/psc/$_" $PSCompletions.download_file("$($PSCompletions.url)/completions/psc/$_", $outFile) if ($outFile -match '\.json$') { $PSCompletions.get_raw_content($outFile) | ConvertFrom-Json | ConvertTo-Json -Compress -Depth 100 | Out-File $outFile -Encoding utf8 -Force } } $PSCompletions.info = $PSCompletions.ConvertFrom_JsonToHashtable($PSCompletions.get_raw_content("$($PSCompletions.path.completions)/psc/language/$language.json")).info } } $PSCompletions.move_old_version() if ($PSCompletions.is_first_init) { $PSCompletions.write_with_color($PSCompletions.replace_content($PSCompletions.info.init_info)) } } $PSCompletions.init_data() if ($PSCompletions.is_init) { $null = $PSCompletions.download_list() } if (!$PSCompletions.config.enable_menu) { Set-PSReadLineKeyHandler $PSCompletions.config.trigger_key MenuComplete } $PSCompletions.generate_completion() $PSCompletions.handle_completion() foreach ($_ in $PSCompletions.data.aliasMap.Keys) { if ($PSCompletions.data.aliasMap[$_] -eq 'psc') { Set-Alias $_ $PSCompletions.config.function_name -ErrorAction SilentlyContinue } else { if ($_ -ne $PSCompletions.data.aliasMap.$_) { Set-Alias $_ $PSCompletions.data.aliasMap.$_ -ErrorAction SilentlyContinue } } } if ($PSCompletions.config.enable_module_update -notin @(0, 1)) { $PSCompletions.version_list = $PSCompletions.config.enable_module_update, $PSCompletions.version | Sort-Object { [version] $_ } -Descending -ErrorAction SilentlyContinue if ($PSCompletions.version_list[0] -ne $PSCompletions.version) { $PSCompletions.wc.DownloadFile("$($PSCompletions.url)/module/CHANGELOG.json", (Join-Path $PSCompletions.path.temp 'CHANGELOG.json')) # XXX: 这里是为了避免 CompletionPredictor 模块引起的多次确认 if (!$PSCompletions._write_update_confirm) { $PSCompletions._write_update_confirm = $true $PSCompletions.write_with_color($PSCompletions.replace_content($PSCompletions.info.module.update)) while (($PSCompletions._PressKey = $host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')).VirtualKeyCode) { if ($PSCompletions._PressKey.ControlKeyState -notlike '*CtrlPressed*') { if ($write_empty_line) { Write-Host '' } if ($PSCompletions._PressKey.VirtualKeyCode -eq 13) { # 13: Enter $PSCompletions._cmd_list = @( "Update-Module PSCompletions -RequiredVersion $($PSCompletions.version_list[0]) -Force -ErrorAction Stop", "Update-PSResource PSCompletions -Version $($PSCompletions.version_list[0]) -Force -ErrorAction Stop", "Scoop update pscompletions" ) foreach ($update_cmd in $PSCompletions._cmd_list) { try { $PSCompletions.write_with_color($PSCompletions.replace_content($PSCompletions.info.module.updating)) Invoke-Expression $update_cmd break } catch { Write-Host $_ -ForegroundColor Red } } } else { $PSCompletions.write_with_color($PSCompletions.replace_content($PSCompletions.info.confirm_cancel)) } break } } } } else { $PSCompletions.config.enable_module_update = 1 $PSCompletions.data | ConvertTo-Json -Depth 100 -Compress | Out-File $PSCompletions.path.data -Force -Encoding utf8 } } else { if (!$PSCompletions._show_update_info) { $PSCompletions._show_update_info = $true if ($PSCompletions.config.enable_completions_update -eq 1) { if ($PSCompletions.update -or $PSCompletions.get_content($PSCompletions.path.change)) { $PSCompletions.write_with_color($PSCompletions.replace_content($PSCompletions.info.update_info)) } } } } $PSCompletions.start_job() |