core/menu/win.ps1
|
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod parse_list { # X if ($config.enable_list_follow_cursor) { $menu.pos.X = $rawUI.CursorPosition.X # 如果跟随鼠标,且超过右侧边界,则向左偏移 $edge = $rawUI.BufferSize.Width - 1 - $menu.ui_width if ($edge -lt $menu.pos.X) { $menu.pos.X = $edge } } else { $menu.pos.X = 0 } # Y $menu.ui_height = $menu.filter_list.Count + 2 if ($menu.is_show_above) { if ($menu.cursor_to_top -lt $menu.ui_height) { $menu.ui_height = $menu.cursor_to_top } $list_limit = if ($config.list_max_count_when_above -eq -1) { 12 }else { $config.list_max_count_when_above + 2 } if ($list_limit -lt $menu.ui_height) { $menu.ui_height = $list_limit } $menu.pos.Y = $rawUI.CursorPosition.Y - $menu.ui_height - $config.height_from_menu_bottom_to_cursor_when_above } else { if ($menu.cursor_to_bottom -lt $menu.ui_height) { $menu.ui_height = $menu.cursor_to_bottom } $list_limit = if ($config.list_max_count_when_below -eq -1) { 12 }else { $config.list_max_count_when_below + 2 } if ($list_limit -lt $menu.ui_height) { $menu.ui_height = $list_limit } $menu.pos.Y = $rawUI.CursorPosition.Y + 1 } $menu.page_max_index = $menu.ui_height - 3 } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod get_buffer { param($startPos, $endPos) $top = New-Object System.Management.Automation.Host.Coordinates $startPos.X, $startPos.Y $bottom = New-Object System.Management.Automation.Host.Coordinates $endPos.X , $endPos.Y $buffer = $rawUI.GetBufferContents((New-Object System.Management.Automation.Host.Rectangle $top, $bottom)) @{ top = $top bottom = $bottom buffer = $buffer } } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_buffer { # XXX: 在 Windows PowerShell 5.x 中,边框使用以下符号以处理兼容性问题 if ($PSEdition -ne 'Core') { $horizontal = '-' $vertical = '|' $top_left = '+' $bottom_left = '+' $top_right = '+' $bottom_right = '+' } else { $horizontal = $config.horizontal $vertical = $config.vertical $top_left = $config.top_left $bottom_left = $config.bottom_left $top_right = $config.top_right $bottom_right = $config.bottom_right } $border_box = @() $content_box = @() $list_area = $menu.list_max_width $border_box += [string]$top_left + $horizontal * $list_area + $top_right $line = [string]$vertical + ' ' * $list_area + [string]$vertical $content = ' ' * $list_area foreach ($_ in 0..($menu.ui_height - 3)) { $border_box += $line $content_box += $content } $status = "$(([string]($menu.selected_index + 1)).PadLeft($menu.filter_list.Count.ToString().Length, ' '))" $border_box += [string]$bottom_left + $horizontal * 2 + ' ' * ($status.Length + 1) + $horizontal * ($list_area - $status.Length - 3) + $bottom_right $rawUI.SetBufferContents($menu.pos, $rawUI.NewBufferCellArray($border_box, $config.border_text, $config.border_back)) $rawUI.SetBufferContents(@{ X = $menu.pos.X + 1 Y = $menu.pos.Y + 1 }, $rawUI.NewBufferCellArray($content_box, $config.item_text, $config.item_back) ) } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_list_buffer { param([int]$offset) $lines = $offset..($menu.ui_height - 3 + $offset) $content_box = foreach ($_ in $lines) { $item = $menu.filter_list[$_] $text = $item.ListItemText + $item.padSymbols $text + ' ' * ($menu.list_max_width - $menu.get_length($text)) } $rawUI.SetBufferContents(@{ X = $menu.pos.X + 1 Y = $menu.pos.Y + 1 }, $rawUI.NewBufferCellArray($content_box, $config.item_text, $config.item_back) ) } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_filter_buffer { param([string]$filter) $char = $config.filter_symbol $middle = [System.Math]::Ceiling($char.Length / 2) $start = $char.Substring(0, $middle) $end = $char.Substring($middle) $rawUI.SetBufferContents( @{ X = $menu.pos.X + 2 Y = $menu.pos.Y }, $rawUI.NewBufferCellArray(@(@($start, $filter, $end) -join ''), $config.filter_text, $config.filter_back) ) } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_status_buffer { $X = $menu.pos.X + 3 if ($menu.is_show_above) { $Y = $rawUI.CursorPosition.Y - 1 - $config.height_from_menu_bottom_to_cursor_when_above } else { $Y = $menu.pos.Y + $menu.ui_height - 1 } $current = "$(([string]($menu.selected_index + 1)).PadLeft($menu.filter_list.Count.ToString().Length, ' '))" $rawUI.SetBufferContents(@{ X = $X; Y = $Y }, $rawUI.NewBufferCellArray(@("$current$($config.status_symbol)$($menu.filter_list.Count)"), $config.status_text, $config.status_back)) } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_tip_buffer { param([int]$index) if ($menu.is_show_above) { $start = 0 $line = $menu.pos.Y } else { $start = $menu.pos.Y + $menu.ui_height $line = $rawUI.BufferSize.Height - $start } if ($line -gt 0) { $content = ' ' * $rawUI.BufferSize.Width $box = foreach ($_ in 1..$line) { $content } $rawUI.SetBufferContents( @{ X = 0 Y = $start }, $rawUI.NewBufferCellArray($box, $bgColor, $bgColor) ) } if ($menu.is_show_tip) { if ($menu.is_show_above) { $rest_line = $menu.cursor_to_top - $menu.ui_height } else { $rest_line = $menu.cursor_to_bottom - $menu.ui_height } if ($rest_line -le 0) { return } $tip = $menu.filter_list[$index].ToolTip if ($tip -ne $null) { $json = $PSCompletions.completions.$($PSCompletions.root_cmd) $info = $json.info $tip_arr = @() $lineWidth = $rawUI.BufferSize.Width if ($config.enable_tip_follow_cursor) { $lineWidth -= $rawUI.CursorPosition.X } $tips = $PSCompletions.replace_content($tip).Split("`n").Where({ $_ -ne '' }) foreach ($v in $tips) { $currentWidth = 0 $outputString = '' $currentLine = '' $char_record = @{} $chars = $v.ToCharArray() foreach ($char in $chars) { if ($char_record.ContainsKey($char)) { $charWidth = $char_record[$char] } else { $charWidth = $rawUI.NewBufferCellArray($char, $bgColor, $bgColor).LongLength $char_record[$char] = $charWidth } if ($currentWidth + $charWidth -gt $lineWidth) { $outputString += $currentLine + "`n" $currentLine = '' $currentWidth = 0 } $currentLine += $char $currentWidth += $charWidth } $outputString += $currentLine $tip_arr += ($outputString).Split("`n") } if ($tip_arr.Count -eq 0) { return } $pos = @{ X = if ($config.enable_tip_follow_cursor) { $menu.pos.X }else { 0 } Y = $menu.pos.Y + $menu.ui_height + 1 } $full = $rest_line - $tip_arr.Count if ($menu.is_show_above) { if ($full -lt 0) { $pos.Y = 0 $maxIndex = $tip_arr.Count + $full - 1 } else { $pos.Y = $full $maxIndex = $tip_arr.Count - 1 } $tip_arr = $tip_arr[0..$maxIndex] } else { if ($pos.Y -ge $rawUI.BufferSize.Height - 1) { return } } $rawUI.SetBufferContents($pos, $rawUI.NewBufferCellArray($tip_arr, $config.tip_text, $config.tip_back)) } } } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod set_selection { if ($menu.old_selection) { $rawUI.SetBufferContents($menu.old_selection.pos, $menu.old_selection.buffer) } $X = $menu.pos.X + 1 $to_X = $X + $menu.list_max_width - 1 # 当前页的第几个 $Y = $menu.pos.Y + 1 + $menu.page_current_index # 根据坐标,生成需要被改变内容的矩形,也就是要被选中的项 $Rectangle = New-Object System.Management.Automation.Host.Rectangle $X, $Y, $to_X, $Y # 通过矩形,获取到这个矩形中的原本的内容 $LineBuffer = $rawUI.GetBufferContents($Rectangle) $menu.old_selection = @{ pos = @{ X = $X Y = $Y } buffer = $LineBuffer } # 给原本的内容设置前景颜色和背景颜色 # XXX: 对于多字节字符,需要过滤掉 Trailing 类型字符以确保正确渲染 $LineBuffer = $LineBuffer.Where({ $_.BufferCellType -ne 'Trailing' }) $content = foreach ($i in $LineBuffer) { $i.Character } $rawUI.SetBufferContents(@{ X = $X; Y = $Y }, $rawUI.NewBufferCellArray(@([string]::Join('', $content)), $config.selected_text, $config.selected_back)) } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod move_selection { param([bool]$isDown) $moveDirection = if ($isDown) { 1 } else { -1 } $is_move = if ($isDown) { $menu.page_current_index -lt $menu.page_max_index } else { $menu.page_current_index -gt 0 } $new_selected_index = $menu.selected_index + $moveDirection if ($config.enable_list_loop) { $menu.selected_index = ($new_selected_index + $menu.filter_list.Count) % $menu.filter_list.Count } else { $menu.selected_index = if ($new_selected_index -lt 0) { 0 } elseif ($new_selected_index -ge $menu.filter_list.Count) { $menu.filter_list.Count - 1 } else { $new_selected_index } } if ($is_move) { $menu.page_current_index = ($menu.page_current_index + $moveDirection) % ($menu.page_max_index + 1) if ($menu.page_current_index -lt 0) { $menu.page_current_index += $menu.page_max_index + 1 } $menu.set_selection() $menu.new_status_buffer() $menu.new_tip_buffer($menu.selected_index) $menu.handle_data('edit') return } if ($config.enable_list_loop -or ($new_selected_index -ge 0 -and $new_selected_index -lt $menu.filter_list.Count)) { if ($isDown) { if ($menu.selected_index -eq 0) { $menu.page_current_index -= $menu.page_max_index } } else { if ($menu.selected_index -eq $menu.filter_list.Count - 1) { $menu.page_current_index += $menu.page_max_index } } $menu.offset = ($menu.offset + $moveDirection) % ($menu.filter_list.Count - $menu.page_max_index) if ($menu.offset -lt 0) { $menu.offset += $menu.filter_list.Count - $menu.page_max_index } $menu.old_selection = $null $menu.new_list_buffer($menu.offset) $menu.set_selection() $menu.new_filter_buffer($menu.filter) $menu.new_status_buffer() $menu.new_tip_buffer($menu.selected_index) $menu.handle_data('edit') } } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod reset { param([bool]$clearAll = $true) if ($clearAll) { $menu.data.Clear() if ($menu.origin_full_buffer) { $rawUI.SetBufferContents($menu.origin_full_buffer.top, $menu.origin_full_buffer.buffer) $menu.origin_full_buffer = $null } } $menu.old_selection = $null $menu.offset = 0 $menu.selected_index = 0 $menu.page_current_index = 0 } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod handle_data { param([string]$type) switch ($type) { add { $menu.data.Add( @{ page_current_index = $menu.page_current_index page_max_index = $menu.page_max_index selected_index = $menu.selected_index offset = $menu.offset filter = $menu.filter filter_list = $menu.filter_list old_selection = $menu.old_selection.Clone() old_full_buffer = $menu.get_buffer($menu.buffer_start, $menu.buffer_end) # XXX: 这里必须使用基础类型,否则有可能出现数据不一致,导致菜单塌陷 ui_height = $menu.ui_height pos_y = $menu.pos.Y } ) } get { $data = $menu.data[-1] $menu.page_current_index = $data.page_current_index $menu.page_max_index = $data.page_max_index $menu.selected_index = $data.selected_index $menu.offset = $data.offset $menu.filter = $data.filter $menu.filter_list = $data.filter_list $menu.old_selection = $data.old_selection # XXX: 这里必须使用基础类型,否则有可能出现数据不一致,导致菜单塌陷 $menu.ui_height = $data.ui_height $menu.pos.Y = $data.pos_y } edit { $menu.data[-1] = @{ page_current_index = $menu.page_current_index page_max_index = $menu.page_max_index selected_index = $menu.selected_index offset = $menu.offset filter = $menu.filter filter_list = $menu.filter_list old_selection = $menu.old_selection.Clone() old_full_buffer = $menu.get_buffer($menu.buffer_start, $menu.buffer_end) # XXX: 这里必须使用基础类型,否则有可能出现数据不一致,导致菜单塌陷 ui_height = $menu.ui_height pos_y = $menu.pos.Y } } } } Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod show_module_menu { param($filter_list) $menu = $PSCompletions.menu $config = $PSCompletions.config $rawUI = $Host.UI.RawUI $bgColor = $rawUI.BackgroundColor if ($rawUI.BufferSize.Height -lt 5) { [Microsoft.PowerShell.PSConsoleReadLine]::UndoAll() [Microsoft.PowerShell.PSConsoleReadLine]::Insert($PSCompletions.info.min_area) return '' } if (!$filter_list) { return '' } $suffix = $config.completion_suffix function handleOutput($item) { $out = $item.CompletionText # 不是由 TabExpansion2 获取的补全,即通过 psc add 添加的补全 if ($null -eq $item.ResultType) { return "$out$suffix" } if ($item.ResultType -in @( [System.Management.Automation.CompletionResultType]::Method, [System.Management.Automation.CompletionResultType]::Property, [System.Management.Automation.CompletionResultType]::Variable, [System.Management.Automation.CompletionResultType]::Type, [System.Management.Automation.CompletionResultType]::Namespace )) { return $out } # Directory, registry key, or other container types if ($item.ResultType -eq [System.Management.Automation.CompletionResultType]::ProviderContainer) { if ($config.enable_path_with_trailing_separator) { if ($out.Length -ge 1 -and $out[-1] -match "^['`"]$") { if ($out.Length -ge 2 -and $out[-2] -match "^[/\\]$") { return $out } return $out.Insert($out.Length - 1, $PSCompletions.separator) } return $out + $PSCompletions.separator } return $out } # File or other leaf items (e.g., registry values) # [System.Management.Automation.CompletionResultType]::ProviderItem # [System.Management.Automation.CompletionResultType]::Command # [System.Management.Automation.CompletionResultType]::ParameterName # [System.Management.Automation.CompletionResultType]::ParameterValue # if foreach switch ... # [System.Management.Automation.CompletionResultType]::Keyword # [System.Management.Automation.CompletionResultType]::Text return "$out$suffix" } $menu.pos = @{ X = 0; Y = 0 } $menu.ui_width = 0 $menu.ui_height = 0 $menu.list_max_width = $config.list_min_width $menu.filter = '' # 过滤的关键词 $menu.page_current_index = 0 # 当前显示页中的索引 $menu.selected_index = 0 # 当前选中项的实际索引 $menu.offset = 0 # 索引的偏移量,用于滚动翻页 # 记录每一次过滤的数据 $menu.data = [System.Collections.Generic.List[System.Object]]::new() if ($menu.by_TabExpansion2) { $menu.is_show_tip = $config.enable_tip_when_enhance } else { $enable_tip = $config.comp_config.$($PSCompletions.root_cmd).enable_tip if ($null -eq $enable_tip) { $menu.is_show_tip = $config.enable_tip } else { $menu.is_show_tip = $enable_tip } } $menu.filter_list = [System.Collections.Generic.List[System.Object]]::new() $maxWidth = $menu.list_max_width foreach ($item in $filter_list) { $len = $menu.get_length($item.ListItemText + $item.padSymbols) if ($len -gt $maxWidth) { $maxWidth = $len } $menu.filter_list.Add($item) } $menu.list_max_width = $maxWidth $menu.ui_width = $maxWidth + 2 if ($config.enable_enter_when_single -and $menu.filter_list.Count -eq 1) { return handleOutput $menu.filter_list[0] } $current_encoding = [console]::OutputEncoding [console]::OutputEncoding = $PSCompletions.encoding $menu.cursor_to_bottom = $rawUI.BufferSize.Height - $rawUI.CursorPosition.Y - 1 $menu.cursor_to_top = $rawUI.CursorPosition.Y - $config.height_from_menu_bottom_to_cursor_when_above - 1 $menu.is_show_above = $menu.cursor_to_bottom -lt $menu.cursor_to_top if ($menu.is_show_above) { $startY = 0 $endY = $menu.cursor_to_top } else { $startY = $rawUI.CursorPosition.Y + 1 $endY = $rawUI.BufferSize.Height - 1 } $menu.buffer_start = New-Object System.Management.Automation.Host.Coordinates 0, $startY $menu.buffer_end = New-Object System.Management.Automation.Host.Coordinates ($rawUI.BufferSize.Width - 1), $endY $menu.parse_list() # 如果解析后的菜单高度小于 3 (上下边框 + 1个补全项) if ($menu.ui_height -lt 3 -or $menu.ui_width -gt $rawUI.BufferSize.Width - 2) { [Microsoft.PowerShell.PSConsoleReadLine]::UndoAll() [Microsoft.PowerShell.PSConsoleReadLine]::Insert($PSCompletions.info.min_area) return '' } # 显示菜单之前,记录 buffer $menu.origin_full_buffer = $menu.get_buffer($menu.buffer_start, $menu.buffer_end) # 显示菜单 $menu.new_buffer() $menu.new_list_buffer($menu.offset) $menu.set_selection() $menu.new_filter_buffer($menu.filter) $menu.new_status_buffer() $menu.new_tip_buffer($menu.selected_index) $menu.handle_data('add') :loop while (($PressKey = $rawUI.ReadKey('NoEcho,IncludeKeyDown,AllowCtrlC')).VirtualKeyCode) { $pressShift = 0x10 -band [int]$PressKey.ControlKeyState $pressCtrl = $PressKey.ControlKeyState -like '*CtrlPressed*' switch ($PressKey.VirtualKeyCode) { 9 { # 9: Tab if ($menu.filter_list.Count -eq 1) { $menu.reset() handleOutput $menu.filter_list[$menu.selected_index] break loop } $menu.move_selection(!$pressShift) break } { $_ -eq 27 -or ($pressCtrl -and $_ -eq 67) } { # 27: ESC # 67: Ctrl + c $menu.reset() '' break loop } { $_ -in @(32, 13) } { # 32: Space # 13: Enter handleOutput $menu.filter_list[$menu.selected_index] $menu.reset() break loop } # 向上 # 37: Up # 38: Left # 85: Ctrl + u # 80: Ctrl + p # 75: Ctrl + k { $_ -in @(37, 38) -or ($pressCtrl -and $_ -in @(85, 80, 75)) } { $menu.move_selection($false) break } # 向下 # 39: Right # 40: Down # 68: Ctrl + d # 78: Ctrl + n # 74: Ctrl + j { $_ -in @(39, 40) -or ($pressCtrl -and $_ -in @(68, 78, 74)) } { $menu.move_selection($true) break } # filter character { $PressKey.Character } { # remove if ($PressKey.Character -eq 8) { # 8: Backspace if ($menu.filter) { $menu.filter = $menu.filter.Substring(0, $menu.filter.Length - 1) } else { $menu.reset() '' break loop } if ($menu.data.Count -gt 1) { $old_buffer = $menu.data[-2].old_full_buffer $rawUI.SetBufferContents($old_buffer.top, $old_buffer.buffer) $menu.data.RemoveAt($menu.data.Count - 1) } else { $old_buffer = $menu.data[0].old_full_buffer $rawUI.SetBufferContents($old_buffer.top, $old_buffer.buffer) $menu.data.Clear() } $menu.handle_data('get') } else { # add if ($menu.filter -match '\*$' -and $PressKey.Character -eq '*') { break } $menu.filter += $PressKey.Character $escapedFilter = $menu.filter -replace '(\[|\])', '`$1' if ($escapedFilter.StartsWith('^')) { $comparison = { param($text) $text -like $escapedFilter.Substring(1) + '*' } } else { $comparison = { param($text) $text -like "*$escapedFilter*" } } $resultList = [System.Collections.Generic.List[System.Object]]::new() $list = $menu.filter_list foreach ($f in $list) { if ($comparison.Invoke($f.ListItemText)) { $resultList.Add($f) } } $menu.filter_list = $resultList.ToArray() if (!$menu.filter_list) { $menu.filter = $menu.data[-1].filter $menu.filter_list = $menu.data[-1].filter_list } else { $menu.reset($false) $menu.parse_list() $menu.new_buffer() $menu.new_list_buffer($menu.offset) $menu.new_tip_buffer($menu.selected_index) $menu.new_status_buffer() $menu.new_filter_buffer($menu.filter) $menu.set_selection(0) $menu.handle_data('add') } } break } } } [console]::OutputEncoding = $current_encoding } |