core/menu/win.ps1

Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod handle_list_first {
    $max_width = 0
    if ($this.is_show_tip) {
        $tip_max_height = 0

        $filterListTasks = @()
        $runspacePool = [runspacefactory]::CreateRunspacePool(1, [Environment]::ProcessorCount)
        $runspacePool.Open()

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

                # 指定一行的最大宽度
                $lineWidth = $Host_UI.RawUI.BufferSize.Width
                if ($PSCompletions.config.menu_tip_follow_cursor) {
                    $w = $lineWidth - $Host_UI.RawUI.CursorPosition.X
                    if ($w -lt $PSCompletions.config.menu_list_min_width) {
                        $lineWidth = $PSCompletions.config.menu_list_min_width
                    }
                    else {
                        $lineWidth = $w
                    }
                }
                $PSCompletions.menu.tip_width = $lineWidth

                # 初始化一些变量
                $currentWidth = 0
                $outputString = ""
                $currentLine = ""

                foreach ($char in $inputString.ToCharArray()) {
                    # 获取当前字符的宽度(1 或 2)
                    $charWidth = if ([System.Text.Encoding]::UTF8.GetByteCount($char) -eq 1) { 1 } else { 2 }

                    # 如果添加这个字符会超过最大宽度,就换行
                    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 }
            }

            if ($PSCompletions.current_cmd) {
                $json = $PSCompletions.data.$($PSCompletions.current_cmd)
                $info = $json.info
            }
            foreach ($item in $items) {
                $tip_arr = @()
                if ($item.ToolTip) {
                    $tip = _replace $item.ToolTip
                    foreach ($v in ($tip -split "`n")) {
                        $tip_arr += (Get-MultilineTruncatedString $v $Host_UI) -split "`n"
                    }
                }
                @{
                    ListItemText   = $item.ListItemText
                    CompletionText = $item.CompletionText
                    ToolTip        = $tip_arr
                }
            }
        }

        foreach ($arr in $PSCompletions.split_array($this.filter_list, [Environment]::ProcessorCount, $true)) {
            $filterListTasks += [powershell]::Create().AddScript($scriptBlock).AddArgument($arr).AddArgument($PSCompletions).AddArgument($Host.UI)
        }

        $runspaces = foreach ($task in $filterListTasks) {
            $task.RunspacePool = $runspacePool
            $job = $task.BeginInvoke()
            @{ Runspace = $task; Job = $job }
        }


        $this.filter_list = [System.Collections.Generic.List[System.Object]]@()
        $this.origin_filter_list = [System.Collections.Generic.List[System.Object]]@()

        foreach ($rs in $runspaces) {
            $results = $rs.Runspace.EndInvoke($rs.Job)
            $rs.Runspace.Dispose()
            foreach ($result in $results) {
                $width = $this.get_length($result.ListItemText)
                if ($width -gt $max_width) { $max_width = $width }
                if ($result.ToolTip.Count -gt $tip_max_height) { $tip_max_height = $result.ToolTip.Count }
                $result = @{
                    ListItemText   = $result.ListItemText
                    CompletionText = $result.CompletionText
                    ToolTip        = $result.ToolTip
                    width          = $width
                    height         = $result.ToolTip.Count
                }
                $this.filter_list.Add($result)
                $this.origin_filter_list.Add($result)
            }
        }

        $runspacePool.Close()
        $runspacePool.Dispose()
        $this.tip_max_height = $tip_max_height
    }
    else {
        $this.filter_list = foreach ($_ in $this.filter_list) {
            $width = $this.get_length($_.ListItemText)
            if ($width -gt $max_width) { $max_width = $width }
            @{
                ListItemText   = $_.ListItemText
                CompletionText = $_.CompletionText
                width          = $width
            }
        }
    }
    if ($max_width -lt $PSCompletions.config.menu_list_min_width) {
        $max_width = $PSCompletions.config.menu_list_min_width
    }
    $this.list_max_width = $max_width

    if ($this.filter_list -is [array]) {
        $PSCompletions.is_single = $false
    }
    else {
        $PSCompletions.is_single = $true
        $this.filter_list = @($this.filter_list)
    }
    $this.ui_size.Width = 1 + $PSCompletions.config.menu_list_margin_left + $max_width + $PSCompletions.config.menu_list_margin_right + 1
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod handle_list {
    $max_width = 0
    if ($this.is_show_tip) {
        $tip_max_height = 0
        foreach ($item in $this.filter_list) {
            if ($item.width -gt $max_width) { $max_width = $item.width }
            if ($item.ToolTip.Count -gt $tip_max_height) { $tip_max_height = $item.ToolTip.Count }
        }
        $this.tip_max_height = $tip_max_height
    }
    else {
        foreach ($item in $this.filter_list) {
            if ($item.width -gt $max_width) { $max_width = $item.width }
        }
    }
    if ($max_width -lt $PSCompletions.config.menu_list_min_width) {
        $max_width = $PSCompletions.config.menu_list_min_width
    }
    $this.list_max_width = $max_width

    if ($this.filter_list -is [array]) {
        $PSCompletions.is_single = $false
    }
    else {
        $PSCompletions.is_single = $true
        $this.filter_list = @($this.filter_list)
    }
    $this.ui_size.Width = 1 + $PSCompletions.config.menu_list_margin_left + $max_width + $PSCompletions.config.menu_list_margin_right + 1
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod parse_list {
    # X
    if ($PSCompletions.config.menu_list_follow_cursor) {
        $this.pos.X = $Host.UI.RawUI.CursorPosition.X
        # 如果跟随鼠标,如果在超过右侧边界,则向左偏移
        if ($this.pos.X + $this.ui_size.Width -gt $Host.UI.RawUI.BufferSize.Width - 1) {
            $this.pos.X = $Host.UI.RawUI.BufferSize.Width - 1 - $this.ui_size.Width
        }
    }
    else {
        $this.pos.X = 0
    }

    # 当为 0 时,有几率出现渲染错误,所以坐标右移一点
    if ($this.pos.x -eq 0) { $this.pos.x ++ }

    if ($PSCompletions.config.menu_tip_follow_cursor) {
        if ($Host.UI.RawUI.BufferSize.Width - $this.pos.X -lt $this.tip_width) {
            $this.pos_tip.X = $Host.UI.RawUI.BufferSize.Width - $this.tip_width
        }
        else {
            $this.pos_tip.X = $this.pos.X
        }
    }
    else {
        $this.pos_tip.X = 0
    }

    # Y
    $this.cursor_to_bottom = $Host.UI.RawUI.BufferSize.Height - $Host.UI.RawUI.CursorPosition.Y - 1
    $this.cursor_to_top = $Host.UI.RawUI.CursorPosition.Y - $PSCompletions.config.menu_above_margin_bottom - 1

    $this.is_show_above = if ($this.cursor_to_bottom -ge $this.cursor_to_top) { $false }else { $true }

    $this.ui_size.height = $this.filter_list.Count + 2
    if ($this.is_show_above) {
        if ($PScompletions.config.menu_above_list_max_count -ne -1) {
            if ($this.filter_list.Count -gt $PScompletions.config.menu_above_list_max_count) {
                $this.ui_size.height = $PScompletions.config.menu_above_list_max_count + 2
            }
        }
        if ($this.ui_size.height -gt $this.cursor_to_top) {
            $this.ui_size.height = $this.cursor_to_top
        }
    }
    else {
        if ($PScompletions.config.menu_below_list_max_count -ne -1) {
            if ($this.filter_list.Count -gt $PScompletions.config.menu_below_list_max_count) {
                $this.ui_size.height = $PScompletions.config.menu_below_list_max_count + 2
            }
        }
        if ($this.ui_size.height -gt $this.cursor_to_bottom) {
            $this.ui_size.height = $this.cursor_to_bottom
        }
    }
    if ($this.is_show_above) {
        $new_ui_height = $this.cursor_to_top - $this.tip_max_height - 1
        if ($new_ui_height -lt 3) {
            $this.is_show_tip = $false
        }
        else {
            $this.ui_size.height = $new_ui_height
            if ($this.ui_size.height - 2 -gt $this.filter_list.Count) {
                $this.ui_size.height = $this.filter_list.Count + 2
            }
        }
        if ($this.ui_size.height -lt 3) { $this.ui_size.height = 3 }
        $this.pos.Y = $Host.UI.RawUI.CursorPosition.Y - $this.ui_size.height - $PSCompletions.config.menu_above_margin_bottom
        # 设置 tip 的 起始位置
        $this.pos_tip.Y = $this.pos.Y - $this.tip_max_height - 1
        if ($this.pos_tip.Y -lt 0) {
            $this.is_show_tip = $false
        }
    }
    else {
        $new_ui_height = $this.cursor_to_bottom - $this.tip_max_height - 1
        if ($new_ui_height -lt 3) {
            $this.is_show_tip = $false
        }
        else {
            $this.ui_size.height = $new_ui_height
            if ($this.ui_size.height - 2 -gt $this.filter_list.Count) {
                $this.ui_size.height = $this.filter_list.Count + 2
            }
        }
        if ($this.ui_size.height -lt 3) { $this.ui_size.height = 3 }
        $this.pos.Y = $Host.UI.RawUI.CursorPosition.Y + 1
        $this.pos_tip.Y = $this.pos.Y + $this.ui_size.height + 1
        if ($this.pos_tip.Y -ge $Host.UI.RawUI.BufferSize.Height) {
            $this.is_show_tip = $false
        }
    }
    $this.page_max_index = $this.ui_size.height - 3
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod get_old_buffer {
    param($X, $Y)
    $old_top = New-Object System.Management.Automation.Host.Coordinates 0, $Y
    $old_bottom = New-Object System.Management.Automation.Host.Coordinates $Host.ui.RawUI.BufferSize.Width, ($Y + $this.ui_size.height)
    $old_buffer = $Host.UI.RawUI.GetBufferContents((New-Object System.Management.Automation.Host.Rectangle $old_top, $old_bottom))
    $this.old_menu_buffer = $old_buffer  # 之前的内容
    $this.old_menu_pos = $old_top  # 之前的位置
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_buffer {
    if ($PSCompletions.config.menu_list_cover_buffer) {
        $box = @()
        foreach ($_ in 0..$this.ui_size.Height) {
            $box += (' ' * $Host.UI.RawUI.BufferSize.Width)
        }
        $pos = @{
            X = 0
            Y = $this.pos.Y
        }
        if ($PSCompletions.menu.is_show_above) { $pos.Y -- }
        $buffer = $Host.UI.RawUI.NewBufferCellArray($box, $host.UI.RawUI.BackgroundColor, $host.UI.RawUI.BackgroundColor)
        $Host.UI.RawUI.SetBufferContents($pos, $buffer)
    }

    $border_box = @()
    $content_box = @()
    $line_top = [string]$PSCompletions.config.menu_line_top_left + $PSCompletions.config.menu_line_horizontal * ($this.list_max_width + $PSCompletions.config.menu_list_margin_left + $PSCompletions.config.menu_list_margin_right) + $PSCompletions.config.menu_line_top_right
    $border_box += $line_top
    foreach ($_ in 0..($this.ui_size.height - 3)) {
        $pad = $this.list_max_width - $this.filter_list[$_].width
        $content = "$($this.filter_list[$_].ListItemText)$(' ' * $pad)"

        $border_box += [string]$PSCompletions.config.menu_line_vertical + (' ' * ($PSCompletions.config.menu_list_margin_left + $this.list_max_width + $PSCompletions.config.menu_list_margin_right)) + [string]$PSCompletions.config.menu_line_vertical
        $content_box += (' ' * $PSCompletions.config.menu_list_margin_left) + $content + (' ' * $PSCompletions.config.menu_list_margin_right)
    }
    $status = "$(([string]($this.selected_index + 1)).PadLeft($this.filter_list.Count.ToString().Length, ' '))"
    $line_bottom = [string]$PSCompletions.config.menu_line_bottom_left + $PSCompletions.config.menu_line_horizontal * 2 + ' ' * ($status.Length + 1) + $PSCompletions.config.menu_line_horizontal * ($this.list_max_width + $PSCompletions.config.menu_list_margin_left + $PSCompletions.config.menu_list_margin_right - $status.Length - 3) + $PSCompletions.config.menu_line_bottom_right
    $border_box += $line_bottom

    $buffer = $Host.UI.RawUI.NewBufferCellArray($border_box, $PSCompletions.config.menu_color_border_text, $PSCompletions.config.menu_color_border_back)
    $Host.UI.RawUI.SetBufferContents($this.pos, $buffer)

    $buffer = $Host.UI.RawUI.NewBufferCellArray($content_box, $PSCompletions.config.menu_color_item_text, $PSCompletions.config.menu_color_item_back)
    $pos = @{
        X = $this.pos.X + 1
        Y = $this.pos.Y + 1
    }
    $Host.UI.RawUI.SetBufferContents($pos, $buffer)
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_list_buffer {
    param($offset)
    $content_box = @()
    foreach ($_ in $offset..($this.ui_size.height - 3 + $offset)) {
        $pad = $this.list_max_width - $this.filter_list[$_].width
        $content = "$($this.filter_list[$_].ListItemText)$(' ' * $pad)"

        $content_box += (' ' * $PSCompletions.config.menu_list_margin_left) + $content + (' ' * $PSCompletions.config.menu_list_margin_right)
    }
    $buffer = $Host.UI.RawUI.NewBufferCellArray($content_box, $PSCompletions.config.menu_color_item_text, $PSCompletions.config.menu_color_item_back)
    $pos = @{
        X = $this.pos.X + 1
        Y = $this.pos.Y + 1
    }
    $Host.UI.RawUI.SetBufferContents($pos, $buffer)
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_filter_buffer {
    param($filter)
    if ($this.old_filter_buffer) {
        $Host.UI.RawUI.SetBufferContents($this.old_filter_pos, $this.old_filter_buffer)
    }
    $X = $this.pos.X
    $Y = $this.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))

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

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

    $buffer_filter = $Host.UI.RawUI.NewBufferCellArray(@(@($start, $filter, $end) -join ''), $PSCompletions.config.menu_color_filter_text, $PSCompletions.config.menu_color_filter_back)
    $pos_filter = $Host.UI.RawUI.CursorPosition
    $pos_filter.X = $this.pos.X + 2
    $pos_filter.Y = $this.pos.Y
    $Host.UI.RawUI.SetBufferContents($pos_filter, $buffer_filter)
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_status_buffer {
    if ($this.old_status_buffer) {
        $Host.UI.RawUI.SetBufferContents($this.old_status_pos, $this.old_status_buffer)
    }
    $X = $this.pos.X
    $Y = $this.pos.Y + $this.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 + 1)

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

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

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

    $pos_status = $Host.UI.RawUI.CursorPosition
    $pos_status.X = $this.pos.X + 3
    $pos_status.Y = $this.pos.Y + $this.ui_size.height - 1
    $Host.UI.RawUI.SetBufferContents($pos_status, $buffer_status)
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod get_old_tip_buffer {
    param($X, $Y)
    if ($PSCompletions.config.menu_tip_cover_buffer) {
        if ($PSCompletions.menu.is_show_above) {
            $Y = 0
            $to_Y = $this.pos.Y
        }
        else {
            $Y = $this.pos_tip.Y - 1
            $to_Y = $Host.UI.RawUI.BufferSize.Height
        }
    }
    else {
        if ($PSCompletions.menu.is_show_above) {
            $Y = $this.pos_tip.Y - 1
            $to_Y = $this.pos.Y
        }
        else {
            $Y = $this.pos_tip.Y - 1
            $to_Y = $this.pos_tip.Y + $this.tip_max_height
        }
    }
    $old_top = New-Object System.Management.Automation.Host.Coordinates 0, $Y
    $old_bottom = New-Object System.Management.Automation.Host.Coordinates $Host.ui.RawUI.BufferSize.Width, $to_Y

    $old_buffer = $Host.UI.RawUI.GetBufferContents((New-Object System.Management.Automation.Host.Rectangle $old_top, $old_bottom))
    $this.old_tip_buffer = $old_buffer
    $this.old_tip_pos = $old_top
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod new_tip_buffer {
    param($index)
    $box = @()
    if ($PSCompletions.config.menu_tip_cover_buffer) {
        if ($PSCompletions.menu.is_show_above) {
            foreach ($_ in 0..($this.pos.Y - 1)) {
                $box += (' ' * $Host.UI.RawUI.BufferSize.Width)
            }
            $pos = @{
                X = 0
                Y = 0
            }
        }
        else {
            foreach ($_ in 0..($Host.UI.RawUI.BufferSize.Height - $this.pos_tip.Y)) {
                $box += (' ' * $Host.UI.RawUI.BufferSize.Width)
            }
            $pos = @{
                X = 0
                Y = $this.pos_tip.Y - 1
            }
        }
    }
    else {
        foreach ($_ in 0..($this.tip_max_height + 1)) {
            $box += (' ' * $Host.UI.RawUI.BufferSize.Width)
        }
        $pos = @{
            X = 0
            Y = $this.pos_tip.Y - 1
        }
        if ($PSCompletions.menu.is_show_above -and $pos.Y -lt 0) { $pos.Y = 0 }
    }
    $buffer = $Host.UI.RawUI.NewBufferCellArray($box, $host.UI.RawUI.BackgroundColor, $host.UI.RawUI.BackgroundColor)
    $Host.UI.RawUI.SetBufferContents($pos, $buffer)

    if ($this.filter_list[$index].ToolTip) {
        $pos = @{
            X = $this.pos_tip.X
            Y = $this.pos_tip.Y
        }
        $buffer = $Host.UI.RawUI.NewBufferCellArray($this.filter_list[$index].ToolTip, $PSCompletions.config.menu_color_tip_text, $PSCompletions.config.menu_color_tip_back)
        $Host.UI.RawUI.SetBufferContents($pos, $buffer)
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod set_selection {
    if ($this.old_selection) {
        $Host.UI.RawUI.SetBufferContents($this.old_selection.pos, $this.old_selection.buffer)
    }
    # 如果高亮需要包含 margin
    if ($PSCompletions.config.menu_selection_with_margin) {
        $X = $this.pos.X + 1
        $to_X = $X + $this.list_max_width - 1 + $PSCompletions.config.menu_list_margin_left + $PSCompletions.config.menu_list_margin_right
    }
    else {
        $X = $this.pos.X + 1 + $PSCompletions.config.menu_list_margin_left
        $to_X = $X + $this.list_max_width - 1
    }

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

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

    # 通过矩形,获取到这个矩形中的原本的内容
    $LineBuffer = $Host.UI.RawUI.GetBufferContents($Rectangle)
    $this.old_selection = @{
        pos    = @{
            X = $X
            Y = $Y
        }
        buffer = $LineBuffer
    }
    # 给原本的内容设置颜色和背景颜色
    # 如果补全项出现多字节字符,就会裁切出 Trailing 类型字符
    # 如果不去掉 Trailing 类型字符的话,菜单渲染问题挺大
    # 去掉后,有多字节字符的行尾的菜单边框会消失,不过在可接受的范围内
    $LineBuffer = $Host.UI.RawUI.NewBufferCellArray(
        @([string]::Join('', ($LineBuffer | Where-Object { $_.BufferCellType -ne "Trailing" } | ForEach-Object { $_.Character }))),
        $PSCompletions.config.menu_color_selected_text,
        $PSCompletions.config.menu_color_selected_back
    )
    $Host.UI.RawUI.SetBufferContents(@{ X = $X; Y = $Y }, $LineBuffer)
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod move_selection {
    param([int]$count)
    $is_scroll = $false
    $is_move = $false
    if ($this.selected_index + $count -ge 0 -and $this.selected_index + $count -le ($this.filter_list.Count - 1) ) {
        if ($count -gt 0) {
            if ($this.page_current_index -ge $this.page_max_index) {
                $is_scroll = $true
            }
            else {
                $is_move = $true
            }
        }
        else {
            if ($this.page_current_index -le 0) {
                $is_scroll = $true
            }
            else {
                $is_move = $true
            }
        }
    }

    if ($is_move) {
        $this.page_current_index += $count
        $this.selected_index += $count
        $this.set_selection()
    }
    if ($is_scroll) {
        $this.selected_index += $count
        $this.offset += $count
        $this.new_list_buffer($this.offset)

        $this.old_selection = $null
        $this.set_selection()
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod get_prefix {
    $prefix = $this.filter_list[-1].ListItemText
    for ($i = $this.filter_list.count - 2; $i -ge 0 -and $prefix; --$i) {
        $text = $this.filter_list[$i].ListItemText
        if ($text) {
            while ($prefix -and !$text.StartsWith($prefix, [StringComparison]::OrdinalIgnoreCase)) {
                $prefix = $prefix.Substring(0, $prefix.Length - 1)
            }
        }
    }
    $this.filter = $this.filter_by_auto_pick = $prefix
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod filter_completions {
    param($filter_list)
    # 如果是前缀匹配
    if ($PSCompletions.config.menu_is_prefix_match) {
        $match = "$($this.filter)*"
    }
    else {
        $match = "*$($this.filter)*"
    }
    $this.filter_list = [System.Collections.Generic.List[System.Object]]@()

    foreach ($f in $filter_list) {
        if ($f.ListItemText -and $f.ListItemText -like $match) {
            $this.filter_list.Add($f)
        }
    }
    $PSCompletions.is_single = $this.filter_list.Count -eq 1
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod reset {
    param([bool]$clearAll, [bool]$is_menu_enhance)
    # reset content
    try {
        $Host.UI.RawUI.SetBufferContents($this.old_tip_pos, $this.old_tip_buffer)
        if ($clearAll) {
            $this.old_tip_buffer = $null
            $this.old_tip_pos = $null
        }
    }
    catch {}
    try {
        $Host.UI.RawUI.SetBufferContents($this.old_menu_pos, $this.old_menu_buffer)
        if ($clearAll) {
            $this.old_menu_buffer = $null
            $this.old_menu_pos = $null
        }
    }
    catch {}
    $this.old_selection = $null
    $this.old_filter_buffer = $null
    $this.old_filter_pos = $null
    $this.old_status_buffer = $null
    $this.old_status_pos = $null

    $this.offset = 0
    $this.selected_index = 0
    $this.page_current_index = 0


    if ($is_menu_enhance) {
        $PSCompletions.menu.is_show_tip = $PSCompletions.config.menu_show_tip_when_enhance -eq 1
    }
    else {
        if ($PSCompletions.current_cmd) {
            $menu_show_tip = $PSCompletions.config.comp_config.$($PSCompletions.current_cmd).menu_show_tip
            if ($menu_show_tip -ne $null) {
                $this.is_show_tip = $menu_show_tip -eq 1
            }
            else {
                $this.is_show_tip = $PSCompletions.config.menu_show_tip -eq 1
            }
        }
        else {
            $this.is_show_tip = $PSCompletions.config.menu_show_tip -eq 1
        }
    }
}
Add-Member -InputObject $PSCompletions.menu -MemberType ScriptMethod show_module_menu {
    param($filter_list, [bool]$is_menu_enhance)
    $this.filter_list = $filter_list

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

    $this.pos = $Host.UI.RawUI.WindowPosition
    $this.pos_tip = $Host.UI.RawUI.WindowPosition
    $this.list_max_width = 0

    $this.ui_size = $Host.UI.RawUI.BufferSize

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

    $this.page_current_index = 0 # 当前显示第几个

    $this.selected_index = 0  # 当前选中的索引
    $this.old_selected_index = 0

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

    $this.filter_completions($this.filter_list)
    # 如果没有可用的选项,直接结束,触发路径补全
    if (!$this.filter_list) { return }
    $this.reset($true, $is_menu_enhance)
    $this.handle_list_first()

    if ($PSCompletions.config.enter_when_single -and $PSCompletions.is_single) {
        return $this.filter_list[$this.selected_index].CompletionText
    }
    $this.parse_list()

    # 如果解析后的菜单高度小于 3 (上下边框 + 1个补全项)
    if ($this.ui_size.Height -lt 3 -or $this.ui_size.Width -gt $Host.ui.RawUI.BufferSize.Width - 2) {
        [Microsoft.PowerShell.PSConsoleReadLine]::UndoAll()
        [Microsoft.PowerShell.PSConsoleReadLine]::Insert($PSCompletions.info.min_area)
        ''
        return
    }
    # 第一次显示菜单之前,记录之前的 buffer 内容
    $this.get_old_buffer($this.pos.X, $this.pos.Y)
    if ($this.is_show_tip) { $this.get_old_tip_buffer($this.pos_tip.X, $this.pos_tip.Y) }
    # 显示菜单
    $this.new_buffer()
    if ($this.is_show_tip) { $this.new_tip_buffer($this.selected_index) }
    if ($PSCompletions.config.menu_is_prefix_match) { $this.get_prefix() }
    $this.new_filter_buffer($this.filter)
    $this.new_status_buffer()
    $this.set_selection()

    try {
        :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
                        $this.reset($true)
                        ''
                        break loop
                    }
                    { $_ -eq 85 -or $_ -eq 80 } {
                        # 85: Ctrl + u
                        # 80: Ctrl + p
                        $this.move_selection(-1)
                        break
                    }

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

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