core/menu/win.ps1

Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod handle_list_first {
    param([array]$filter_list)
    if ($filter_list.Count -ge 1000) {
        $PSCompletions.menu.filter_list = $PSCompletions.handle_data_by_runspace($filter_list, {
                param ($items, $PSCompletions, $Host_UI)

                if ($PSCompletions.root_cmd -ne $null) {
                    $json = $PSCompletions.completions.$($PSCompletions.root_cmd)
                    $info = $json.info
                }
                $return = @()

                if ($PSCompletions.menu.is_show_tip) {
                    function Get-MultilineTruncatedString {
                        param ([string]$inputString, $Host_UI = $Host.UI)

                        $lineWidth = $Host_UI.RawUI.BufferSize.Width

                        if ($PSCompletions.config.enable_tip_follow_cursor -eq 1) {
                            $lineWidth -= $Host_UI.RawUI.CursorPosition.X
                        }

                        $currentWidth = 0
                        $outputString = ''
                        $currentLine = ''

                        $char_record = @{}

                        foreach ($char in $inputString.ToCharArray()) {
                            if ($char_record.ContainsKey($char)) {
                                $charWidth = $char_record[$char]
                            }
                            else {
                                $charWidth = $Host_UI.RawUI.NewBufferCellArray($char, $Host_UI.RawUI.BackgroundColor, $Host_UI.RawUI.BackgroundColor).LongLength
                                $char_record[$char] = $charWidth
                            }

                            if ($currentWidth + $charWidth -gt $lineWidth) {
                                $outputString += $currentLine + "`n"
                                $currentLine = ''
                                $currentWidth = 0
                            }
                            $currentLine += $char
                            $currentWidth += $charWidth
                        }
                        $outputString += $currentLine

                        return $outputString
                    }
                    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 }
                    }
                    foreach ($item in $items) {
                        $tip_arr = @()
                        if ($item.ToolTip -ne $null) {
                            $tip = _replace $item.ToolTip
                            foreach ($v in ($tip -split "`n")) {
                                $tip_arr += (Get-MultilineTruncatedString $v $Host_UI) -split "`n"
                            }
                        }
                        $return += @{
                            ListItemText   = $item.ListItemText
                            CompletionText = $item.CompletionText
                            ToolTip        = $tip_arr
                        }
                    }
                }
                else {
                    $return += $items
                }
                return $return
            }, {
                param($results)
                $return = @()
                if ($PSCompletions.menu.is_show_tip) {
                    foreach ($result in $results) {
                        $PSCompletions.menu.tip_max_height = [Math]::Max($PSCompletions.menu.tip_max_height, $result.ToolTip.Count)
                        $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $PSCompletions.menu.get_length($result.ListItemText))
                        $return += @{
                            ListItemText   = $result.ListItemText
                            CompletionText = $result.CompletionText
                            ToolTip        = $result.ToolTip
                        }
                    }
                }
                else {
                    foreach ($result in $results) {
                        $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $PSCompletions.menu.get_length($result.ListItemText))
                        $return += @{
                            ListItemText   = $result.ListItemText
                            CompletionText = $result.CompletionText
                            ToolTip        = $result.ToolTip
                        }
                    }
                }
                return $return
            })
    }
    else {
        function Get-MultilineTruncatedString {
            param ([string]$inputString, $Host_UI = $Host.UI)

            $lineWidth = $Host_UI.RawUI.BufferSize.Width

            if ($PSCompletions.config.enable_tip_follow_cursor -eq 1) {
                $lineWidth -= $Host_UI.RawUI.CursorPosition.X
            }

            $currentWidth = 0
            $outputString = ''
            $currentLine = ''

            $char_record = @{}

            foreach ($char in $inputString.ToCharArray()) {
                if ($char_record.ContainsKey($char)) {
                    $charWidth = $char_record[$char]
                }
                else {
                    $charWidth = $Host_UI.RawUI.NewBufferCellArray($char, $Host_UI.RawUI.BackgroundColor, $Host_UI.RawUI.BackgroundColor).LongLength
                    $char_record[$char] = $charWidth
                }

                if ($currentWidth + $charWidth -gt $lineWidth) {
                    $outputString += $currentLine + "`n"
                    $currentLine = ''
                    $currentWidth = 0
                }
                $currentLine += $char
                $currentWidth += $charWidth
            }
            $outputString += $currentLine

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

        $json = $PSCompletions.completions.$($PSCompletions.root_cmd)
        $info = $json.info
        $results = @()

        if ($PSCompletions.menu.is_show_tip) {
            foreach ($item in $filter_list) {
                $tip_arr = @()
                if ($item.ToolTip -ne $null) {
                    $tip = _replace $item.ToolTip
                    foreach ($v in ($tip -split "`n")) {
                        $tip_arr += (Get-MultilineTruncatedString $v $Host.UI) -split "`n"
                    }
                }
                $PSCompletions.menu.tip_max_height = [Math]::Max($PSCompletions.menu.tip_max_height, $tip_arr.Count)
                $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $PSCompletions.menu.get_length($item.ListItemText))
                $results += @{
                    ListItemText   = $item.ListItemText
                    CompletionText = $item.CompletionText
                    ToolTip        = $tip_arr
                }
            }
        }
        else {
            foreach ($item in $filter_list) {
                $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $PSCompletions.menu.get_length($item.ListItemText))
                $results += @{
                    ListItemText   = $item.ListItemText
                    CompletionText = $item.CompletionText
                }
            }
        }
        $PSCompletions.menu.filter_list = $results
    }
    $PSCompletions.menu.origin_filter_list = $PSCompletions.menu.filter_list.Clone()
    $PSCompletions.menu.list_max_width = [Math]::Max($PSCompletions.menu.list_max_width, $PSCompletions.config.list_min_width)
    $PSCompletions.menu.ui_size.Width = $PSCompletions.menu.list_max_width + 2 + $PSCompletions.menu.config.width_from_menu_left_to_item + $PSCompletions.menu.config.width_from_menu_right_to_item
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod parse_list {
    # X
    if ($PSCompletions.config.enable_list_follow_cursor -eq 1) {
        $PSCompletions.menu.pos.X = $Host.UI.RawUI.CursorPosition.X
        # 如果跟随鼠标,且超过右侧边界,则向左偏移
        $PSCompletions.menu.pos.X = [Math]::Min($PSCompletions.menu.pos.X, $Host.UI.RawUI.BufferSize.Width - 1 - $PSCompletions.menu.ui_size.Width)
    }
    else {
        $PSCompletions.menu.pos.X = 0
    }

    if ($PSCompletions.config.enable_tip_follow_cursor -eq 1) {
        $PSCompletions.menu.pos_tip.X = $PSCompletions.menu.pos.X
    }
    else {
        $PSCompletions.menu.pos_tip.X = 0
    }

    # Y
    $PSCompletions.menu.ui_size.Height = $PSCompletions.menu.filter_list.Count + 2
    if ($PSCompletions.menu.is_show_above) {
        if ($PSCompletions.config.list_max_count_when_above -eq -1) {
            $PSCompletions.menu.ui_size.Height = [Math]::Min($PSCompletions.menu.cursor_to_top, $PSCompletions.menu.ui_size.Height)

            $menu_limit = 4
            # 8: 2个补全项 + 3行提示的高度
            $rest = [Math]::Max(0, $PSCompletions.menu.cursor_to_top - 8)
            if ($rest -le 1) {
                $menu_limit += $rest + ($PSCompletions.menu.cursor_to_top -gt 4)
            }
            else {
                $r = $rest / 2
                if ($r -gt $PSCompletions.menu.tip_max_height) {
                    $menu_limit += $rest - $PSCompletions.menu.tip_max_height
                }
                else {
                    $menu_limit += $r
                }
            }
            if ($PSCompletions.menu.cursor_to_top -eq 6) {
                $menu_limit = 4
            }
        }
        else {
            $PSCompletions.menu.ui_size.Height = [Math]::Min($PSCompletions.menu.cursor_to_top, $PSCompletions.config.list_max_count_when_above + 2)
        }
    }
    else {
        if ($PSCompletions.config.list_max_count_when_below -eq -1) {
            $PSCompletions.menu.ui_size.Height = [Math]::Min($PSCompletions.menu.cursor_to_bottom, $PSCompletions.menu.ui_size.Height)

            $menu_limit = 4
            # 8: 2个补全项 + 3行提示的高度
            $rest = [Math]::Max(0, $PSCompletions.menu.cursor_to_bottom - 8)
            if ($rest -le 1) {
                $menu_limit += $rest + ($PSCompletions.menu.cursor_to_bottom -gt 4)
                $PSCompletions.menu.tip_max_height = $PSCompletions.menu.cursor_to_bottom - $menu_limit - 1
                if ($PSCompletions.menu.tip_max_height -lt 1) {
                    $PSCompletions.menu.tip_max_height = 1
                }
            }
            else {
                $r = $rest / 2
                if ($r -gt $PSCompletions.menu.tip_max_height) {
                    $menu_limit += $rest - $PSCompletions.menu.tip_max_height
                }
                else {
                    $menu_limit += $r
                }
            }
            if ($PSCompletions.menu.cursor_to_bottom -eq 6) {
                $menu_limit = 4
            }
        }
        else {
            $PSCompletions.menu.ui_size.Height = [Math]::Min($PSCompletions.menu.cursor_to_bottom, $PSCompletions.config.list_max_count_when_below + 2)
        }
    }

    if ($PSCompletions.menu.is_show_tip) {
        $menu_limit = [Math]::Min($menu_limit, $PSCompletions.menu.ui_size.Height)
        if ($PSCompletions.menu.is_show_above) {
            if ($menu_limit -gt 12 -and ($PSCompletions.menu.cursor_to_top - $menu_limit) -lt $PSCompletions.menu.tip_max_height) {
                $menu_limit = 12
                $PSCompletions.menu.tip_max_height = $PSCompletions.menu.cursor_to_top - $menu_limit - 1
            }
            else {
                # tip 可以显示的高度
                $rest = $PSCompletions.menu.cursor_to_top - $menu_limit - 1
                if ($PSCompletions.menu.tip_max_height -gt $rest) {
                    $PSCompletions.menu.tip_max_height = [Math]::Max(1, $rest)
                }
            }
            $PSCompletions.menu.ui_size.Height = [Math]::Min($menu_limit, $PSCompletions.menu.ui_size.Height)
            $PSCompletions.menu.pos.Y = $Host.UI.RawUI.CursorPosition.Y - $PSCompletions.menu.ui_size.Height - $PSCompletions.config.height_from_menu_bottom_to_cursor_when_above
            $PSCompletions.menu.pos_tip.Y = $PSCompletions.menu.pos.Y - $PSCompletions.menu.tip_max_height - 1
            if ($PSCompletions.menu.pos_tip.Y -lt 0) {
                $PSCompletions.menu.is_show_tip = $false
            }
        }
        else {
            if ($menu_limit -gt 12 -and ($PSCompletions.menu.cursor_to_bottom - $menu_limit) -lt $PSCompletions.menu.tip_max_height) {
                $menu_limit = 12
            }
            $PSCompletions.menu.ui_size.Height = [Math]::Min($menu_limit, $PSCompletions.menu.ui_size.Height)
            $PSCompletions.menu.pos.Y = $Host.UI.RawUI.CursorPosition.Y + 1
            $PSCompletions.menu.pos_tip.Y = $PSCompletions.menu.pos.Y + $PSCompletions.menu.ui_size.Height + 1
            if ($PSCompletions.menu.pos_tip.Y -gt $Host.UI.RawUI.BufferSize.Height - 1) {
                $PSCompletions.menu.is_show_tip = $false
            }
        }
    }
    else {
        if ($PSCompletions.menu.is_show_above) {
            $PSCompletions.menu.ui_size.Height = [Math]::Min($PSCompletions.menu.cursor_to_top, $PSCompletions.menu.ui_size.Height)
            $PSCompletions.menu.pos.Y = $Host.UI.RawUI.CursorPosition.Y - $PSCompletions.menu.ui_size.Height - $PSCompletions.config.height_from_menu_bottom_to_cursor_when_above
        }
        else {
            $PSCompletions.menu.ui_size.Height = [Math]::Min($PSCompletions.menu.cursor_to_bottom, $PSCompletions.menu.ui_size.Height)
            $PSCompletions.menu.pos.Y = $Host.UI.RawUI.CursorPosition.Y + 1
        }
    }
    $PSCompletions.menu.page_max_index = $PSCompletions.menu.ui_size.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 = $Host.UI.RawUI.GetBufferContents((New-Object System.Management.Automation.Host.Rectangle $top, $bottom))
    @{
        top    = $top
        bottom = $bottom
        buffer = $buffer
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod get_pos {
    if ($PSCompletions.menu.is_show_above) {
        $menu_start_pos = @{
            X = $Host.UI.RawUI.WindowPosition.X
            Y = 0
        }
        $menu_end_pos = @{
            X = $PSCompletions.menu.pos.X + $PSCompletions.menu.ui_size.Width
            Y = $Host.UI.RawUI.CursorPosition.Y - 1 - $PSCompletions.config.height_from_menu_bottom_to_cursor_when_above
        }
        $menu_start_pos.Y = [Math]::Max($menu_start_pos.Y, $PSCompletions.menu.pos.Y)
    }
    else {
        $menu_start_pos = $PSCompletions.menu.pos
        $menu_end_pos = @{
            X = $menu_start_pos.X + $PSCompletions.menu.ui_size.Width
            Y = $menu_start_pos.Y + $PSCompletions.menu.ui_size.Height
        }
        $menu_end_pos.Y = [Math]::Min($menu_end_pos.Y, $Host.UI.RawUI.BufferSize.Height - 1)
    }
    if ($PSCompletions.menu.is_show_tip) {
        $tip_start_pos = @{
            X = $Host.UI.RawUI.WindowPosition.X
            Y = $PSCompletions.menu.pos.Y - $PSCompletions.menu.tip_max_height - 1
        }
        if ($PSCompletions.menu.is_show_above) {
            $tip_start_pos.Y = [Math]::Min($tip_start_pos.Y, $Host.UI.RawUI.WindowPosition.Y)
            $tip_end_pos = @{
                X = $Host.UI.RawUI.BufferSize.Width
                Y = $PSCompletions.menu.pos.Y
            }
        }
        else {
            $tip_start_pos.Y = $PSCompletions.menu.pos_tip.Y
            $tip_end_pos = @{
                X = $Host.UI.RawUI.BufferSize.Width
                Y = $PSCompletions.menu.pos_tip.Y + $PSCompletions.menu.tip_max_height + 1
            }
            $tip_end_pos.Y = [Math]::Min($tip_end_pos.Y, $Host.UI.RawUI.BufferSize.Height - 1)
        }
    }
    @{
        menu = @{
            start_pos = $menu_start_pos
            end_pos   = $menu_end_pos
        }
        tip  = @{
            start_pos = $tip_start_pos
            end_pos   = $tip_end_pos
        }
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_cover_buffer {
    if ($PSCompletions.config.enable_tip_cover_buffer -eq 1) {
        $box = @()
        $line = ' ' * $Host.UI.RawUI.BufferSize.Width
        if ($PSCompletions.menu.is_show_above) {
            foreach ($_ in 0..($PSCompletions.menu.pos.Y - 2)) {
                $box += $line
            }
            $pos = $Host.UI.RawUI.WindowPosition
        }
        else {
            foreach ($_ in 0..($Host.UI.RawUI.BufferSize.Height - $PSCompletions.menu.pos_tip.Y)) {
                $box += $line
            }
            $pos = @{
                X = 0
                Y = $PSCompletions.menu.pos_tip.Y - 1
            }
            $pos.Y = [Math]::Min($pos.Y, $Host.UI.RawUI.BufferSize.Height - 1)
        }
        $Host.UI.RawUI.SetBufferContents($pos, $Host.UI.RawUI.NewBufferCellArray($box, $Host.UI.RawUI.BackgroundColor, $Host.UI.RawUI.BackgroundColor))
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_buffer {
    # XXX: 在 Windows PowerShell 5.x 中,始终覆盖菜单缓冲区,以处理兼容性问题
    if ($PSCompletions.config.enable_list_cover_buffer -eq 1 -or $PSEdition -ne 'Core') {
        $box = @()
        $line = ' ' * $Host.UI.RawUI.BufferSize.Width
        foreach ($_ in 0..$PSCompletions.menu.ui_size.Height) {
            $box += $line
        }
        $pos = @{
            X = 0
            Y = $PSCompletions.menu.pos.Y
        }
        if ($PSCompletions.menu.is_show_above) { $pos.Y -- }
        $Host.UI.RawUI.SetBufferContents($pos, $Host.UI.RawUI.NewBufferCellArray($box, $Host.UI.RawUI.BackgroundColor, $Host.UI.RawUI.BackgroundColor))
    }

    # XXX: 在 Windows PowerShell 5.x 中,边框使用以下符号以处理兼容性问题
    if ($PSEdition -ne 'Core') {
        $horizontal = '-'
        $vertical = '|'
        $top_left = '+'
        $bottom_left = '+'
        $top_right = '+'
        $bottom_right = '+'
    }
    else {
        $horizontal = $PSCompletions.config.horizontal
        $vertical = $PSCompletions.config.vertical
        $top_left = $PSCompletions.config.top_left
        $bottom_left = $PSCompletions.config.bottom_left
        $top_right = $PSCompletions.config.top_right
        $bottom_right = $PSCompletions.config.bottom_right
    }

    $border_box = @()
    $content_box = @()

    $border_box += [string]$top_left + $horizontal * ($PSCompletions.menu.list_max_width + $PSCompletions.config.width_from_menu_left_to_item + $PSCompletions.config.width_from_menu_right_to_item) + $top_right

    $line = [string]$vertical + (' ' * ($PSCompletions.config.width_from_menu_left_to_item + $PSCompletions.menu.list_max_width + $PSCompletions.config.width_from_menu_right_to_item)) + [string]$vertical
    $left = ' ' * $PSCompletions.config.width_from_menu_left_to_item
    $right = ' ' * $PSCompletions.config.width_from_menu_right_to_item
    foreach ($_ in 0..($PSCompletions.menu.ui_size.Height - 3)) {
        $content_length = $PSCompletions.menu.get_length($PSCompletions.menu.filter_list[$_].ListItemText)
        $content = $PSCompletions.menu.filter_list[$_].ListItemText + ' ' * ($PSCompletions.menu.list_max_width - $content_length)
        $border_box += $line
        $content_box += $left + $content + $right
    }

    $status = "$(([string]($PSCompletions.menu.selected_index + 1)).PadLeft($PSCompletions.menu.filter_list.Count.ToString().Length, ' '))"

    $border_box += [string]$bottom_left + $horizontal * 2 + ' ' * ($status.Length + 1) + $horizontal * ($PSCompletions.menu.list_max_width + $PSCompletions.config.width_from_menu_left_to_item + $PSCompletions.config.width_from_menu_right_to_item - $status.Length - 3) + $bottom_right

    $Host.UI.RawUI.SetBufferContents($PSCompletions.menu.pos, $Host.UI.RawUI.NewBufferCellArray($border_box, $PSCompletions.config.border_text, $PSCompletions.config.border_back))

    $pos = @{
        X = $PSCompletions.menu.pos.X + 1
        Y = $PSCompletions.menu.pos.Y + 1
    }
    $Host.UI.RawUI.SetBufferContents($pos, $Host.UI.RawUI.NewBufferCellArray($content_box, $PSCompletions.config.item_text, $PSCompletions.config.item_back))
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_list_buffer {
    param([int]$offset)
    $content_box = @()
    $left = ' ' * $PSCompletions.config.width_from_menu_left_to_item
    $right = ' ' * $PSCompletions.config.width_from_menu_right_to_item
    foreach ($_ in $offset..($PSCompletions.menu.ui_size.Height - 3 + $offset)) {
        $content_length = $PSCompletions.menu.get_length($PSCompletions.menu.filter_list[$_].ListItemText)
        $content = $PSCompletions.menu.filter_list[$_].ListItemText + ' ' * ($PSCompletions.menu.list_max_width - $content_length)
        $content_box += $left + $content + $right
    }
    $pos = @{
        X = $PSCompletions.menu.pos.X + 1
        Y = $PSCompletions.menu.pos.Y + 1
    }
    $Host.UI.RawUI.SetBufferContents($pos, $Host.UI.RawUI.NewBufferCellArray($content_box, $PSCompletions.config.item_text, $PSCompletions.config.item_back))
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_filter_buffer {
    param([string]$filter)
    if ($PSCompletions.menu.old_filter_buffer) {
        $Host.UI.RawUI.SetBufferContents($PSCompletions.menu.old_filter_pos, $PSCompletions.menu.old_filter_buffer)
    }
    $X = $PSCompletions.menu.pos.X
    $Y = $PSCompletions.menu.pos.Y

    $old_top = New-Object System.Management.Automation.Host.Coordinates $X, $Y
    $old_bottom = New-Object System.Management.Automation.Host.Coordinates $Host.ui.RawUI.BufferSize.Width, $Y

    $old_buffer = $Host.UI.RawUI.GetBufferContents((New-Object System.Management.Automation.Host.Rectangle $old_top, $old_bottom))

    $PSCompletions.menu.old_filter_buffer = $old_buffer  # 之前的内容
    $PSCompletions.menu.old_filter_pos = $old_top  # 之前的位置

    $char = $PSCompletions.config.filter_symbol
    $middle = [System.Math]::Ceiling($char.Length / 2)
    $start = $char.Substring(0, $middle)
    $end = $char.Substring($middle)

    $buffer_filter = $Host.UI.RawUI.NewBufferCellArray(@(@($start, $filter, $end) -join ''), $PSCompletions.config.filter_text, $PSCompletions.config.filter_back)
    $pos_filter = $Host.UI.RawUI.CursorPosition
    $pos_filter.X = $PSCompletions.menu.pos.X + 2
    $pos_filter.Y = $PSCompletions.menu.pos.Y
    $Host.UI.RawUI.SetBufferContents($pos_filter, $buffer_filter)
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_status_buffer {
    if ($PSCompletions.menu.old_status_buffer) {
        $Host.UI.RawUI.SetBufferContents($PSCompletions.menu.old_status_pos, $PSCompletions.menu.old_status_buffer)
    }
    $X = $PSCompletions.menu.pos.X + 3
    if ($PSCompletions.menu.is_show_above) {
        $Y = $Host.UI.RawUI.CursorPosition.Y - 1 - $PSCompletions.config.height_from_menu_bottom_to_cursor_when_above
    }
    else {
        $Y = $PSCompletions.menu.pos.Y + $PSCompletions.menu.ui_size.Height - 1
    }

    $old_top = New-Object System.Management.Automation.Host.Coordinates $X, $Y
    $old_bottom = New-Object System.Management.Automation.Host.Coordinates $Host.UI.RawUI.BufferSize.Width, $Y

    $old_buffer = $Host.UI.RawUI.GetBufferContents((New-Object System.Management.Automation.Host.Rectangle $old_top, $old_bottom))

    $PSCompletions.menu.old_status_buffer = $old_buffer  # 之前的内容
    $PSCompletions.menu.old_status_pos = $old_top  # 之前的位置

    $current = "$(([string]($PSCompletions.menu.selected_index + 1)).PadLeft($PSCompletions.menu.filter_list.Count.ToString().Length, ' '))"
    $buffer_status = $Host.UI.RawUI.NewBufferCellArray(@("$current$($PSCompletions.config.status_symbol)$($PSCompletions.menu.filter_list.Count)"), $PSCompletions.config.status_text, $PSCompletions.config.status_back)

    $Host.UI.RawUI.SetBufferContents(@{ X = $X; Y = $Y }, $buffer_status)
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod get_old_tip_buffer {
    param([int]$X, [int]$Y)
    if ($PSCompletions.config.enable_tip_cover_buffer -eq 1) {
        if ($PSCompletions.menu.is_show_above) {
            $Y = 0
            $to_Y = $PSCompletions.menu.pos.Y
        }
        else {
            $Y = $PSCompletions.menu.pos_tip.Y - 1
            $to_Y = $Host.UI.RawUI.BufferSize.Height - 1
        }
    }
    else {
        $Y = $PSCompletions.menu.pos_tip.Y - 1
        if ($PSCompletions.menu.is_show_above) {
            $to_Y = $PSCompletions.menu.pos.Y
        }
        else {
            $to_Y = $PSCompletions.menu.pos_tip.Y + $PSCompletions.menu.tip_max_height
        }
    }
    $PSCompletions.menu.old_tip_buffer = $PSCompletions.menu.get_buffer(@{ X = 0; Y = $Y }, @{ X = $Host.UI.RawUI.BufferSize.Width; Y = $to_Y })
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_tip_buffer {
    param([int]$index)
    $box = @()
    $line = ' ' * $Host.UI.RawUI.BufferSize.Width
    if ($PSCompletions.config.enable_tip_cover_buffer -eq 1) {
        if ($PSCompletions.menu.is_show_above) {
            foreach ($_ in 0..($PSCompletions.menu.pos.Y - 1)) {
                $box += $line
            }
            $pos = @{
                X = 0
                Y = 0
            }
        }
        else {
            foreach ($_ in 0..($Host.UI.RawUI.BufferSize.Height - $PSCompletions.menu.pos_tip.Y)) {
                $box += $line
            }
            $pos = @{
                X = 0
                Y = $PSCompletions.menu.pos_tip.Y - 1
            }
        }
    }
    else {
        foreach ($_ in 0..($PSCompletions.menu.tip_max_height + 1)) {
            $box += $line
        }
        $pos = @{
            X = 0
            Y = $PSCompletions.menu.pos_tip.Y - 1
        }
        if ($PSCompletions.menu.is_show_above) {
            $pos.Y = [Math]::Max($pos.Y, 0)
        }
    }
    $Host.UI.RawUI.SetBufferContents($pos, $Host.UI.RawUI.NewBufferCellArray($box, $Host.UI.RawUI.BackgroundColor, $Host.UI.RawUI.BackgroundColor))

    if ($PSCompletions.menu.filter_list[$index].ToolTip -ne $null) {
        $pos = @{
            X = $PSCompletions.menu.pos_tip.X
            Y = $PSCompletions.menu.pos_tip.Y
        }
        $tip = $PSCompletions.menu.filter_list[$index].ToolTip
        if ($PSCompletions.menu.tip_max_height -eq 1) {
            if ($tip.Count -gt 1) {
                $tip = $tip[0]
            }
        }
        else {
            if ($tip.Count -gt $PSCompletions.menu.tip_max_height) {
                $tip = $tip[0..($PSCompletions.menu.tip_max_height - 1)]
            }
        }
        $Host.UI.RawUI.SetBufferContents($pos, $Host.UI.RawUI.NewBufferCellArray($tip, $PSCompletions.config.tip_text, $PSCompletions.config.tip_back))
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod set_selection {
    if ($PSCompletions.menu.old_selection) {
        $Host.UI.RawUI.SetBufferContents($PSCompletions.menu.old_selection.pos, $PSCompletions.menu.old_selection.buffer)
    }

    $X = $PSCompletions.menu.pos.X + 1
    $to_X = $X + $PSCompletions.menu.list_max_width - 1
    # 如果选中高亮需要包含 margin
    if ($PSCompletions.config.enable_selection_with_margin -eq 1) {
        $to_X += $PSCompletions.config.width_from_menu_left_to_item + $PSCompletions.config.width_from_menu_right_to_item
    }
    else {
        $X += $PSCompletions.config.width_from_menu_left_to_item
    }

    # 当前页的第几个
    $Y = $PSCompletions.menu.pos.Y + 1 + $PSCompletions.menu.page_current_index

    # 根据坐标,生成需要被改变内容的矩形,也就是要被选中的项
    $Rectangle = New-Object System.Management.Automation.Host.Rectangle $X, $Y, $to_X, $Y

    # 通过矩形,获取到这个矩形中的原本的内容
    $LineBuffer = $Host.UI.RawUI.GetBufferContents($Rectangle)
    $PSCompletions.menu.old_selection = @{
        pos    = @{
            X = $X
            Y = $Y
        }
        buffer = $LineBuffer
    }
    # 给原本的内容设置颜色和背景颜色
    # XXX: 对于多字节字符,需要过滤掉 Trailing 类型字符以确保正确渲染
    # $content = foreach ($i in $LineBuffer) { $i.Character }
    $content = foreach ($i in $LineBuffer.Where({ $_.BufferCellType -ne 'Trailing' })) { $i.Character }
    $LineBuffer = $Host.UI.RawUI.NewBufferCellArray(
        @([string]::Join('', $content)),
        $PSCompletions.config.selected_text,
        $PSCompletions.config.selected_back
    )
    $Host.UI.RawUI.SetBufferContents(@{ X = $X; Y = $Y }, $LineBuffer)
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod move_selection {
    param([bool]$isDown)

    $moveDirection = if ($isDown) { 1 } else { -1 }

    $is_move = if ($isDown) {
        $PSCompletions.menu.page_current_index -lt $PSCompletions.menu.page_max_index
    }
    else {
        $PSCompletions.menu.page_current_index -gt 0
    }

    $new_selected_index = $PSCompletions.menu.selected_index + $moveDirection

    if ($PSCompletions.config.enable_list_loop -eq 1) {
        $PSCompletions.menu.selected_index = ($new_selected_index + $PSCompletions.menu.filter_list.Count) % $PSCompletions.menu.filter_list.Count
    }
    else {
        # Handle no loop
        $PSCompletions.menu.selected_index = if ($new_selected_index -lt 0) {
            0
        }
        elseif ($new_selected_index -ge $PSCompletions.menu.filter_list.Count) {
            $PSCompletions.menu.filter_list.Count - 1
        }
        else {
            $new_selected_index
        }
    }

    if ($is_move) {
        $PSCompletions.menu.page_current_index = ($PSCompletions.menu.page_current_index + $moveDirection) % ($PSCompletions.menu.page_max_index + 1)
        if ($PSCompletions.menu.page_current_index -lt 0) {
            $PSCompletions.menu.page_current_index += $PSCompletions.menu.page_max_index + 1
        }
    }
    elseif ($PSCompletions.config.enable_list_loop -eq 1 -or ($new_selected_index -ge 0 -and $new_selected_index -lt $PSCompletions.menu.filter_list.Count)) {
        if (!$isDown -and $PSCompletions.menu.selected_index -eq $PSCompletions.menu.filter_list.Count - 1) {
            $PSCompletions.menu.page_current_index += $PSCompletions.menu.page_max_index
        }
        elseif ($isDown -and $PSCompletions.menu.selected_index -eq 0) {
            $PSCompletions.menu.page_current_index -= $PSCompletions.menu.page_max_index
        }

        $PSCompletions.menu.offset = ($PSCompletions.menu.offset + $moveDirection) % ($PSCompletions.menu.filter_list.Count - $PSCompletions.menu.page_max_index)
        if ($PSCompletions.menu.offset -lt 0) {
            $PSCompletions.menu.offset += $PSCompletions.menu.filter_list.Count - $PSCompletions.menu.page_max_index
        }

        $PSCompletions.menu.new_list_buffer($PSCompletions.menu.offset)
        $PSCompletions.menu.old_selection = $null
    }
    $PSCompletions.menu.set_selection()
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod get_prefix {
    $prefix = $PSCompletions.menu.filter_list[-1].CompletionText
    for ($i = $PSCompletions.menu.filter_list.count - 2; $i -ge 0 -and $prefix; --$i) {
        $text = $PSCompletions.menu.filter_list[$i].CompletionText
        if ($text -ne $null) {
            while ($prefix -and !$text.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) {
                $prefix = $prefix.Substring(0, $prefix.Length - 1)
            }
        }
    }
    $PSCompletions.menu.filter = $PSCompletions.menu.filter_by_auto_pick = $prefix
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod filter_completions {
    param([array]$filter_list)
    # 如果是前缀匹配
    if ($PSCompletions.config.enable_prefix_match_in_filter -eq 1) {
        $match = "$([WildcardPattern]::Escape($PSCompletions.menu.filter))*"
    }
    else {
        $match = "*$([WildcardPattern]::Escape($PSCompletions.menu.filter))*"
    }
    $PSCompletions.menu.filter_list = @()

    foreach ($f in $filter_list) {
        if ($f.CompletionText -and $f.CompletionText -like $match) {
            $PSCompletions.menu.filter_list += $f
        }
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod reset {
    param([bool]$clearAll)

    if ($PSCompletions.menu.old_tip_buffer) {
        $Host.UI.RawUI.SetBufferContents($PSCompletions.menu.old_tip_buffer.top, $PSCompletions.menu.old_tip_buffer.buffer)
    }

    if ($PSCompletions.menu.old_menu_buffer) {
        $Host.UI.RawUI.SetBufferContents($PSCompletions.menu.old_menu_buffer.top, $PSCompletions.menu.old_menu_buffer.buffer)
    }

    if ($clearAll) {
        $PSCompletions.menu.old_tip_buffer = $null
        $PSCompletions.menu.old_menu_buffer = $null
        if ($PSCompletions.menu.origin_menu_buffer) {
            $Host.UI.RawUI.SetBufferContents($PSCompletions.menu.origin_menu_buffer.top, $PSCompletions.menu.origin_menu_buffer.buffer)
            $PSCompletions.menu.origin_menu_buffer = $null
        }
        if ($PSCompletions.menu.origin_tip_buffer) {
            $Host.UI.RawUI.SetBufferContents($PSCompletions.menu.origin_tip_buffer.top, $PSCompletions.menu.origin_tip_buffer.buffer)
            $PSCompletions.menu.origin_tip_buffer = $null
        }
    }
    $PSCompletions.menu.old_selection = $null
    $PSCompletions.menu.old_filter_buffer = $null
    $PSCompletions.menu.old_filter_pos = $null
    $PSCompletions.menu.old_status_buffer = $null
    $PSCompletions.menu.old_status_pos = $null

    $PSCompletions.menu.offset = 0
    $PSCompletions.menu.selected_index = 0
    $PSCompletions.menu.page_current_index = 0

    if ($PSCompletions.menu.by_TabExpansion2) {
        $PSCompletions.menu.is_show_tip = $PSCompletions.config.enable_tip_when_enhance -eq 1
    }
    else {
        $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
        }
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod show_module_menu {
    param($filter_list)
    if ($Host.UI.RawUI.BufferSize.Height -lt 5) {
        [Microsoft.PowerShell.PSConsoleReadLine]::UndoAll()
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($PSCompletions.info.min_area)
        ''
        return
    }
    if (!$filter_list) { return }
    function handleOutput($item) {
        $out = $item.CompletionText
        try {
            if ((Get-ItemProperty ($item.ToolTip)).Attributes -like '*Directory*') {
                if ($out[-1] -match "^['`"]$") {
                    if ($out[-2] -match "^[/\\]$") {
                        return $out
                    }
                    return $out.Insert($out.Length - 1, $PSCompletions.separator)
                }
                if ($out[-1] -match "^[/\\]$") {
                    return $out
                }
                return $out + $PSCompletions.separator
            }
        }
        catch {}
        return $out
    }

    $PSCompletions.menu.pos = @{ X = 0; Y = 0 }
    $PSCompletions.menu.pos_tip = @{ X = 0; Y = 0 }

    $PSCompletions.menu.list_max_width = $PSCompletions.config.list_min_width
    $PSCompletions.menu.tip_max_height = 1
    $PSCompletions.menu.ui_size = $Host.UI.RawUI.BufferSize

    $PSCompletions.menu.filter = ''  # 过滤的关键词
    $PSCompletions.menu.old_filter = ''

    $PSCompletions.menu.page_current_index = 0 # 当前显示页中的索引

    $PSCompletions.menu.selected_index = 0  # 当前选中项的实际索引
    $PSCompletions.menu.old_selected_index = 0

    $PSCompletions.menu.offset = 0  # 索引的偏移量,用于滚动翻页

    $PSCompletions.menu.reset($true)

    $PSCompletions.menu.handle_list_first($filter_list)

    if ($PSCompletions.config.enable_enter_when_single -eq 1 -and $PSCompletions.menu.filter_list.Count -eq 1) {
        return handleOutput $PSCompletions.menu.filter_list[0]
    }

    $current_encoding = [console]::OutputEncoding
    [console]::OutputEncoding = $PSCompletions.encoding

    $PSCompletions.menu.cursor_to_bottom = $Host.UI.RawUI.BufferSize.Height - 1 - $Host.UI.RawUI.CursorPosition.Y
    $PSCompletions.menu.cursor_to_top = $Host.UI.RawUI.CursorPosition.Y - $PSCompletions.config.height_from_menu_bottom_to_cursor_when_above - 1

    $PSCompletions.menu.is_show_above = $PSCompletions.menu.cursor_to_bottom -lt $PSCompletions.menu.cursor_to_top

    $PSCompletions.menu.parse_list()

    # 如果解析后的菜单高度小于 3 (上下边框 + 1个补全项)
    if ($PSCompletions.menu.ui_size.Height -lt 3 -or $PSCompletions.menu.ui_size.Width -gt $Host.ui.RawUI.BufferSize.Width - 2) {
        [Microsoft.PowerShell.PSConsoleReadLine]::UndoAll()
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($PSCompletions.info.min_area)
        ''
        return
    }

    # 显示菜单之前,记录 buffer
    $pos = $PSCompletions.menu.get_pos()
    $PSCompletions.menu.origin_menu_buffer = $PSCompletions.menu.get_buffer($pos.menu.start_pos, $pos.menu.end_pos)

    $PSCompletions.menu.origin_tip_buffer = $PSCompletions.menu.get_buffer($pos.tip.start_pos, $pos.tip.end_pos)

    $PSCompletions.menu.old_menu_buffer = $PSCompletions.menu.get_buffer(
        @{
            X = $Host.UI.RawUI.WindowPosition.X
            Y = $PSCompletions.menu.pos.Y
        },
        @{
            X = $Host.ui.RawUI.BufferSize.Width
            Y = $PSCompletions.menu.pos.Y + $PSCompletions.menu.ui_size.Height - $PSCompletions.menu.is_show_above
        })

    if ($PSCompletions.menu.is_show_tip) { $PSCompletions.menu.get_old_tip_buffer($PSCompletions.menu.pos_tip.X, $PSCompletions.menu.pos_tip.Y) }
    # 显示菜单
    $PSCompletions.menu.new_buffer()
    if ($PSCompletions.menu.is_show_tip) { $PSCompletions.menu.new_tip_buffer($PSCompletions.menu.selected_index) }
    if ($PSCompletions.config.enable_prefix_match_in_filter -eq 1) { $PSCompletions.menu.get_prefix() }
    $PSCompletions.menu.new_filter_buffer($PSCompletions.menu.filter)
    $PSCompletions.menu.new_status_buffer()
    $PSCompletions.menu.set_selection()
    $old_filter_list = $PSCompletions.menu.filter_list
    :loop while (($PressKey = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown,AllowCtrlC')).VirtualKeyCode) {
        $shift_pressed = 0x10 -band [int]$PressKey.ControlKeyState
        if ($PressKey.ControlKeyState -like '*CtrlPressed*') {
            switch ($PressKey.VirtualKeyCode) {
                67 {
                    # 67: Ctrl + c
                    $PSCompletions.menu.reset($true)
                    ''
                    break loop
                }
                { $_ -eq 85 -or $_ -eq 80 } {
                    # 85: Ctrl + u
                    # 80: Ctrl + p
                    $PSCompletions.menu.move_selection($false)
                    break
                }

                { $_ -eq 68 -or $_ -eq 78 } {
                    # 68: Ctrl + d
                    # 78: Ctrl + n
                    $PSCompletions.menu.move_selection($true)
                    break
                }
            }
        }
        switch ($PressKey.VirtualKeyCode) {
            { $_ -in @(9, 32) } {
                # 9: Tab
                # 32: Space
                if ($PSCompletions.menu.filter_list.Count -eq 1) {
                    $PSCompletions.menu.reset($true)
                    $PSCompletions.menu.filter_list[$PSCompletions.menu.selected_index].CompletionText
                    break loop
                }
                if ($shift_pressed) {
                    # Up
                    $PSCompletions.menu.move_selection($false)
                }
                else {
                    # Down
                    $PSCompletions.menu.move_selection($true)
                }
                break
            }
            27 {
                # 27: ESC
                $PSCompletions.menu.reset($true)
                ''
                break loop
            }
            13 {
                # 13: Enter
                handleOutput $PSCompletions.menu.filter_list[$PSCompletions.menu.selected_index]
                $PSCompletions.menu.reset($true)
                break loop
            }

            # 向上
            # 37: Up
            # 38: Left
            { $_ -in @(37, 38) } {
                $PSCompletions.menu.move_selection($false)
                break
            }
            # 向下
            # 39: Right
            # 40: Down
            { $_ -in @(39, 40) } {
                $PSCompletions.menu.move_selection($true)
                break
            }
            # Character/Backspace
            { $PressKey.Character } {
                # update filter
                if ($PressKey.Character -eq 8) {
                    # 8: backspace
                    if ($PSCompletions.menu.filter -eq $PSCompletions.menu.filter_by_auto_pick) {
                        $PSCompletions.menu.filter = ''
                        $PSCompletions.menu.filter_by_auto_pick = ''
                        $PSCompletions.menu.reset($true)
                        ''
                        break loop
                    }
                    if ($PSCompletions.menu.filter) {
                        $PSCompletions.menu.filter = $PSCompletions.menu.filter.Substring(0, $PSCompletions.menu.filter.Length - 1)
                    }
                    else {
                        $PSCompletions.menu.reset($true)
                        ''
                        break loop
                    }
                    $PSCompletions.menu.new_cover_buffer()
                    $PSCompletions.menu.reset()
                    $PSCompletions.menu.filter_completions($PSCompletions.menu.origin_filter_list)
                    $PSCompletions.menu.parse_list()
                    $PSCompletions.menu.new_buffer()
                    if ($PSCompletions.menu.is_show_tip) { $PSCompletions.menu.new_tip_buffer($PSCompletions.menu.selected_index) }
                    $PSCompletions.menu.new_status_buffer()
                    $PSCompletions.menu.set_selection(0)
                }
                else {
                    # add char
                    $PSCompletions.menu.filter += $PressKey.Character
                    $PSCompletions.menu.filter_completions($PSCompletions.menu.filter_list)
                    if (!$PSCompletions.menu.filter_list) {
                        $PSCompletions.menu.filter = $PSCompletions.menu.old_filter
                        $PSCompletions.menu.filter_list = $old_filter_list
                    }
                    else {
                        $PSCompletions.menu.new_cover_buffer()
                        $PSCompletions.menu.reset()
                        $PSCompletions.menu.parse_list()
                        $PSCompletions.menu.new_buffer()
                        if ($PSCompletions.menu.is_show_tip) { $PSCompletions.menu.new_tip_buffer($PSCompletions.menu.selected_index) }
                        $PSCompletions.menu.new_status_buffer()
                        $PSCompletions.menu.set_selection(0)
                    }
                }
                break
            }
        }
        if ($PSCompletions.menu.selected_index -ne $PSCompletions.menu.old_selected_index) {
            $PSCompletions.menu.new_status_buffer()
            if ($PSCompletions.menu.is_show_tip) { $PSCompletions.menu.new_tip_buffer($PSCompletions.menu.selected_index) }
            $PSCompletions.menu.old_selected_index = $PSCompletions.menu.selected_index
        }
        if ($PSCompletions.menu.filter -ne $PSCompletions.menu.old_filter) {
            $PSCompletions.menu.new_filter_buffer($PSCompletions.menu.filter)
            $PSCompletions.menu.old_filter = $PSCompletions.menu.filter
            $old_filter_list = $PSCompletions.menu.filter_list
        }
    }
    [console]::OutputEncoding = $current_encoding
}