1from __future__ import annotations 
    2 
    3import math 
    4from itertools import zip_longest 
    5from typing import TYPE_CHECKING, Callable, Iterable, Sequence, TypeVar, cast 
    6from weakref import WeakKeyDictionary 
    7 
    8from prompt_toolkit.application.current import get_app 
    9from prompt_toolkit.buffer import CompletionState 
    10from prompt_toolkit.completion import Completion 
    11from prompt_toolkit.data_structures import Point 
    12from prompt_toolkit.filters import ( 
    13    Condition, 
    14    FilterOrBool, 
    15    has_completions, 
    16    is_done, 
    17    to_filter, 
    18) 
    19from prompt_toolkit.formatted_text import ( 
    20    StyleAndTextTuples, 
    21    fragment_list_width, 
    22    to_formatted_text, 
    23) 
    24from prompt_toolkit.key_binding.key_processor import KeyPressEvent 
    25from prompt_toolkit.layout.utils import explode_text_fragments 
    26from prompt_toolkit.mouse_events import MouseEvent, MouseEventType 
    27from prompt_toolkit.utils import get_cwidth 
    28 
    29from .containers import ConditionalContainer, HSplit, ScrollOffsets, Window 
    30from .controls import GetLinePrefixCallable, UIContent, UIControl 
    31from .dimension import Dimension 
    32from .margins import ScrollbarMargin 
    33 
    34if TYPE_CHECKING: 
    35    from prompt_toolkit.key_binding.key_bindings import ( 
    36        KeyBindings, 
    37        NotImplementedOrNone, 
    38    ) 
    39 
    40 
    41__all__ = [ 
    42    "CompletionsMenu", 
    43    "MultiColumnCompletionsMenu", 
    44] 
    45 
    46E = KeyPressEvent 
    47 
    48 
    49class CompletionsMenuControl(UIControl): 
    50    """ 
    51    Helper for drawing the complete menu to the screen. 
    52 
    53    :param scroll_offset: Number (integer) representing the preferred amount of 
    54        completions to be displayed before and after the current one. When this 
    55        is a very high number, the current completion will be shown in the 
    56        middle most of the time. 
    57    """ 
    58 
    59    # Preferred minimum size of the menu control. 
    60    # The CompletionsMenu class defines a width of 8, and there is a scrollbar 
    61    # of 1.) 
    62    MIN_WIDTH = 7 
    63 
    64    def has_focus(self) -> bool: 
    65        return False 
    66 
    67    def preferred_width(self, max_available_width: int) -> int | None: 
    68        complete_state = get_app().current_buffer.complete_state 
    69        if complete_state: 
    70            menu_width = self._get_menu_width(500, complete_state) 
    71            menu_meta_width = self._get_menu_meta_width(500, complete_state) 
    72 
    73            return menu_width + menu_meta_width 
    74        else: 
    75            return 0 
    76 
    77    def preferred_height( 
    78        self, 
    79        width: int, 
    80        max_available_height: int, 
    81        wrap_lines: bool, 
    82        get_line_prefix: GetLinePrefixCallable | None, 
    83    ) -> int | None: 
    84        complete_state = get_app().current_buffer.complete_state 
    85        if complete_state: 
    86            return len(complete_state.completions) 
    87        else: 
    88            return 0 
    89 
    90    def create_content(self, width: int, height: int) -> UIContent: 
    91        """ 
    92        Create a UIContent object for this control. 
    93        """ 
    94        complete_state = get_app().current_buffer.complete_state 
    95        if complete_state: 
    96            completions = complete_state.completions 
    97            index = complete_state.complete_index  # Can be None! 
    98 
    99            # Calculate width of completions menu. 
    100            menu_width = self._get_menu_width(width, complete_state) 
    101            menu_meta_width = self._get_menu_meta_width( 
    102                width - menu_width, complete_state 
    103            ) 
    104            show_meta = self._show_meta(complete_state) 
    105 
    106            def get_line(i: int) -> StyleAndTextTuples: 
    107                c = completions[i] 
    108                is_current_completion = i == index 
    109                result = _get_menu_item_fragments( 
    110                    c, is_current_completion, menu_width, space_after=True 
    111                ) 
    112 
    113                if show_meta: 
    114                    result += self._get_menu_item_meta_fragments( 
    115                        c, is_current_completion, menu_meta_width 
    116                    ) 
    117                return result 
    118 
    119            return UIContent( 
    120                get_line=get_line, 
    121                cursor_position=Point(x=0, y=index or 0), 
    122                line_count=len(completions), 
    123            ) 
    124 
    125        return UIContent() 
    126 
    127    def _show_meta(self, complete_state: CompletionState) -> bool: 
    128        """ 
    129        Return ``True`` if we need to show a column with meta information. 
    130        """ 
    131        return any(c.display_meta_text for c in complete_state.completions) 
    132 
    133    def _get_menu_width(self, max_width: int, complete_state: CompletionState) -> int: 
    134        """ 
    135        Return the width of the main column. 
    136        """ 
    137        return min( 
    138            max_width, 
    139            max( 
    140                self.MIN_WIDTH, 
    141                max(get_cwidth(c.display_text) for c in complete_state.completions) + 2, 
    142            ), 
    143        ) 
    144 
    145    def _get_menu_meta_width( 
    146        self, max_width: int, complete_state: CompletionState 
    147    ) -> int: 
    148        """ 
    149        Return the width of the meta column. 
    150        """ 
    151 
    152        def meta_width(completion: Completion) -> int: 
    153            return get_cwidth(completion.display_meta_text) 
    154 
    155        if self._show_meta(complete_state): 
    156            # If the amount of completions is over 200, compute the width based 
    157            # on the first 200 completions, otherwise this can be very slow. 
    158            completions = complete_state.completions 
    159            if len(completions) > 200: 
    160                completions = completions[:200] 
    161 
    162            return min(max_width, max(meta_width(c) for c in completions) + 2) 
    163        else: 
    164            return 0 
    165 
    166    def _get_menu_item_meta_fragments( 
    167        self, completion: Completion, is_current_completion: bool, width: int 
    168    ) -> StyleAndTextTuples: 
    169        if is_current_completion: 
    170            style_str = "class:completion-menu.meta.completion.current" 
    171        else: 
    172            style_str = "class:completion-menu.meta.completion" 
    173 
    174        text, tw = _trim_formatted_text(completion.display_meta, width - 2) 
    175        padding = " " * (width - 1 - tw) 
    176 
    177        return to_formatted_text( 
    178            cast(StyleAndTextTuples, []) + [("", " ")] + text + [("", padding)], 
    179            style=style_str, 
    180        ) 
    181 
    182    def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: 
    183        """ 
    184        Handle mouse events: clicking and scrolling. 
    185        """ 
    186        b = get_app().current_buffer 
    187 
    188        if mouse_event.event_type == MouseEventType.MOUSE_UP: 
    189            # Select completion. 
    190            b.go_to_completion(mouse_event.position.y) 
    191            b.complete_state = None 
    192 
    193        elif mouse_event.event_type == MouseEventType.SCROLL_DOWN: 
    194            # Scroll up. 
    195            b.complete_next(count=3, disable_wrap_around=True) 
    196 
    197        elif mouse_event.event_type == MouseEventType.SCROLL_UP: 
    198            # Scroll down. 
    199            b.complete_previous(count=3, disable_wrap_around=True) 
    200 
    201        return None 
    202 
    203 
    204def _get_menu_item_fragments( 
    205    completion: Completion, 
    206    is_current_completion: bool, 
    207    width: int, 
    208    space_after: bool = False, 
    209) -> StyleAndTextTuples: 
    210    """ 
    211    Get the style/text tuples for a menu item, styled and trimmed to the given 
    212    width. 
    213    """ 
    214    if is_current_completion: 
    215        style_str = f"class:completion-menu.completion.current {completion.style} {completion.selected_style}" 
    216    else: 
    217        style_str = "class:completion-menu.completion " + completion.style 
    218 
    219    text, tw = _trim_formatted_text( 
    220        completion.display, (width - 2 if space_after else width - 1) 
    221    ) 
    222 
    223    padding = " " * (width - 1 - tw) 
    224 
    225    return to_formatted_text( 
    226        cast(StyleAndTextTuples, []) + [("", " ")] + text + [("", padding)], 
    227        style=style_str, 
    228    ) 
    229 
    230 
    231def _trim_formatted_text( 
    232    formatted_text: StyleAndTextTuples, max_width: int 
    233) -> tuple[StyleAndTextTuples, int]: 
    234    """ 
    235    Trim the text to `max_width`, append dots when the text is too long. 
    236    Returns (text, width) tuple. 
    237    """ 
    238    width = fragment_list_width(formatted_text) 
    239 
    240    # When the text is too wide, trim it. 
    241    if width > max_width: 
    242        result = []  # Text fragments. 
    243        remaining_width = max_width - 3 
    244 
    245        for style_and_ch in explode_text_fragments(formatted_text): 
    246            ch_width = get_cwidth(style_and_ch[1]) 
    247 
    248            if ch_width <= remaining_width: 
    249                result.append(style_and_ch) 
    250                remaining_width -= ch_width 
    251            else: 
    252                break 
    253 
    254        result.append(("", "...")) 
    255 
    256        return result, max_width - remaining_width 
    257    else: 
    258        return formatted_text, width 
    259 
    260 
    261class CompletionsMenu(ConditionalContainer): 
    262    # NOTE: We use a pretty big z_index by default. Menus are supposed to be 
    263    #       above anything else. We also want to make sure that the content is 
    264    #       visible at the point where we draw this menu. 
    265    def __init__( 
    266        self, 
    267        max_height: int | None = None, 
    268        scroll_offset: int | Callable[[], int] = 0, 
    269        extra_filter: FilterOrBool = True, 
    270        display_arrows: FilterOrBool = False, 
    271        z_index: int = 10**8, 
    272    ) -> None: 
    273        extra_filter = to_filter(extra_filter) 
    274        display_arrows = to_filter(display_arrows) 
    275 
    276        super().__init__( 
    277            content=Window( 
    278                content=CompletionsMenuControl(), 
    279                width=Dimension(min=8), 
    280                height=Dimension(min=1, max=max_height), 
    281                scroll_offsets=ScrollOffsets(top=scroll_offset, bottom=scroll_offset), 
    282                right_margins=[ScrollbarMargin(display_arrows=display_arrows)], 
    283                dont_extend_width=True, 
    284                style="class:completion-menu", 
    285                z_index=z_index, 
    286            ), 
    287            # Show when there are completions but not at the point we are 
    288            # returning the input. 
    289            filter=extra_filter & has_completions & ~is_done, 
    290        ) 
    291 
    292 
    293class MultiColumnCompletionMenuControl(UIControl): 
    294    """ 
    295    Completion menu that displays all the completions in several columns. 
    296    When there are more completions than space for them to be displayed, an 
    297    arrow is shown on the left or right side. 
    298 
    299    `min_rows` indicates how many rows will be available in any possible case. 
    300    When this is larger than one, it will try to use less columns and more 
    301    rows until this value is reached. 
    302    Be careful passing in a too big value, if less than the given amount of 
    303    rows are available, more columns would have been required, but 
    304    `preferred_width` doesn't know about that and reports a too small value. 
    305    This results in less completions displayed and additional scrolling. 
    306    (It's a limitation of how the layout engine currently works: first the 
    307    widths are calculated, then the heights.) 
    308 
    309    :param suggested_max_column_width: The suggested max width of a column. 
    310        The column can still be bigger than this, but if there is place for two 
    311        columns of this width, we will display two columns. This to avoid that 
    312        if there is one very wide completion, that it doesn't significantly 
    313        reduce the amount of columns. 
    314    """ 
    315 
    316    _required_margin = 3  # One extra padding on the right + space for arrows. 
    317 
    318    def __init__(self, min_rows: int = 3, suggested_max_column_width: int = 30) -> None: 
    319        assert min_rows >= 1 
    320 
    321        self.min_rows = min_rows 
    322        self.suggested_max_column_width = suggested_max_column_width 
    323        self.scroll = 0 
    324 
    325        # Cache for column width computations. This computation is not cheap, 
    326        # so we don't want to do it over and over again while the user 
    327        # navigates through the completions. 
    328        # (map `completion_state` to `(completion_count, width)`. We remember 
    329        # the count, because a completer can add new completions to the 
    330        # `CompletionState` while loading.) 
    331        self._column_width_for_completion_state: WeakKeyDictionary[ 
    332            CompletionState, tuple[int, int] 
    333        ] = WeakKeyDictionary() 
    334 
    335        # Info of last rendering. 
    336        self._rendered_rows = 0 
    337        self._rendered_columns = 0 
    338        self._total_columns = 0 
    339        self._render_pos_to_completion: dict[tuple[int, int], Completion] = {} 
    340        self._render_left_arrow = False 
    341        self._render_right_arrow = False 
    342        self._render_width = 0 
    343 
    344    def reset(self) -> None: 
    345        self.scroll = 0 
    346 
    347    def has_focus(self) -> bool: 
    348        return False 
    349 
    350    def preferred_width(self, max_available_width: int) -> int | None: 
    351        """ 
    352        Preferred width: prefer to use at least min_rows, but otherwise as much 
    353        as possible horizontally. 
    354        """ 
    355        complete_state = get_app().current_buffer.complete_state 
    356        if complete_state is None: 
    357            return 0 
    358 
    359        column_width = self._get_column_width(complete_state) 
    360        result = int( 
    361            column_width 
    362            * math.ceil(len(complete_state.completions) / float(self.min_rows)) 
    363        ) 
    364 
    365        # When the desired width is still more than the maximum available, 
    366        # reduce by removing columns until we are less than the available 
    367        # width. 
    368        while ( 
    369            result > column_width 
    370            and result > max_available_width - self._required_margin 
    371        ): 
    372            result -= column_width 
    373        return result + self._required_margin 
    374 
    375    def preferred_height( 
    376        self, 
    377        width: int, 
    378        max_available_height: int, 
    379        wrap_lines: bool, 
    380        get_line_prefix: GetLinePrefixCallable | None, 
    381    ) -> int | None: 
    382        """ 
    383        Preferred height: as much as needed in order to display all the completions. 
    384        """ 
    385        complete_state = get_app().current_buffer.complete_state 
    386        if complete_state is None: 
    387            return 0 
    388 
    389        column_width = self._get_column_width(complete_state) 
    390        column_count = max(1, (width - self._required_margin) // column_width) 
    391 
    392        return int(math.ceil(len(complete_state.completions) / float(column_count))) 
    393 
    394    def create_content(self, width: int, height: int) -> UIContent: 
    395        """ 
    396        Create a UIContent object for this menu. 
    397        """ 
    398        complete_state = get_app().current_buffer.complete_state 
    399        if complete_state is None: 
    400            return UIContent() 
    401 
    402        column_width = self._get_column_width(complete_state) 
    403        self._render_pos_to_completion = {} 
    404 
    405        _T = TypeVar("_T") 
    406 
    407        def grouper( 
    408            n: int, iterable: Iterable[_T], fillvalue: _T | None = None 
    409        ) -> Iterable[Sequence[_T | None]]: 
    410            "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx" 
    411            args = [iter(iterable)] * n 
    412            return zip_longest(fillvalue=fillvalue, *args) 
    413 
    414        def is_current_completion(completion: Completion) -> bool: 
    415            "Returns True when this completion is the currently selected one." 
    416            return ( 
    417                complete_state is not None 
    418                and complete_state.complete_index is not None 
    419                and c == complete_state.current_completion 
    420            ) 
    421 
    422        # Space required outside of the regular columns, for displaying the 
    423        # left and right arrow. 
    424        HORIZONTAL_MARGIN_REQUIRED = 3 
    425 
    426        # There should be at least one column, but it cannot be wider than 
    427        # the available width. 
    428        column_width = min(width - HORIZONTAL_MARGIN_REQUIRED, column_width) 
    429 
    430        # However, when the columns tend to be very wide, because there are 
    431        # some very wide entries, shrink it anyway. 
    432        if column_width > self.suggested_max_column_width: 
    433            # `column_width` can still be bigger that `suggested_max_column_width`, 
    434            # but if there is place for two columns, we divide by two. 
    435            column_width //= column_width // self.suggested_max_column_width 
    436 
    437        visible_columns = max(1, (width - self._required_margin) // column_width) 
    438 
    439        columns_ = list(grouper(height, complete_state.completions)) 
    440        rows_ = list(zip(*columns_)) 
    441 
    442        # Make sure the current completion is always visible: update scroll offset. 
    443        selected_column = (complete_state.complete_index or 0) // height 
    444        self.scroll = min( 
    445            selected_column, max(self.scroll, selected_column - visible_columns + 1) 
    446        ) 
    447 
    448        render_left_arrow = self.scroll > 0 
    449        render_right_arrow = self.scroll < len(rows_[0]) - visible_columns 
    450 
    451        # Write completions to screen. 
    452        fragments_for_line = [] 
    453 
    454        for row_index, row in enumerate(rows_): 
    455            fragments: StyleAndTextTuples = [] 
    456            middle_row = row_index == len(rows_) // 2 
    457 
    458            # Draw left arrow if we have hidden completions on the left. 
    459            if render_left_arrow: 
    460                fragments.append(("class:scrollbar", "<" if middle_row else " ")) 
    461            elif render_right_arrow: 
    462                # Reserve one column empty space. (If there is a right 
    463                # arrow right now, there can be a left arrow as well.) 
    464                fragments.append(("", " ")) 
    465 
    466            # Draw row content. 
    467            for column_index, c in enumerate(row[self.scroll :][:visible_columns]): 
    468                if c is not None: 
    469                    fragments += _get_menu_item_fragments( 
    470                        c, is_current_completion(c), column_width, space_after=False 
    471                    ) 
    472 
    473                    # Remember render position for mouse click handler. 
    474                    for x in range(column_width): 
    475                        self._render_pos_to_completion[ 
    476                            (column_index * column_width + x, row_index) 
    477                        ] = c 
    478                else: 
    479                    fragments.append(("class:completion", " " * column_width)) 
    480 
    481            # Draw trailing padding for this row. 
    482            # (_get_menu_item_fragments only returns padding on the left.) 
    483            if render_left_arrow or render_right_arrow: 
    484                fragments.append(("class:completion", " ")) 
    485 
    486            # Draw right arrow if we have hidden completions on the right. 
    487            if render_right_arrow: 
    488                fragments.append(("class:scrollbar", ">" if middle_row else " ")) 
    489            elif render_left_arrow: 
    490                fragments.append(("class:completion", " ")) 
    491 
    492            # Add line. 
    493            fragments_for_line.append( 
    494                to_formatted_text(fragments, style="class:completion-menu") 
    495            ) 
    496 
    497        self._rendered_rows = height 
    498        self._rendered_columns = visible_columns 
    499        self._total_columns = len(columns_) 
    500        self._render_left_arrow = render_left_arrow 
    501        self._render_right_arrow = render_right_arrow 
    502        self._render_width = ( 
    503            column_width * visible_columns + render_left_arrow + render_right_arrow + 1 
    504        ) 
    505 
    506        def get_line(i: int) -> StyleAndTextTuples: 
    507            return fragments_for_line[i] 
    508 
    509        return UIContent(get_line=get_line, line_count=len(rows_)) 
    510 
    511    def _get_column_width(self, completion_state: CompletionState) -> int: 
    512        """ 
    513        Return the width of each column. 
    514        """ 
    515        try: 
    516            count, width = self._column_width_for_completion_state[completion_state] 
    517            if count != len(completion_state.completions): 
    518                # Number of completions changed, recompute. 
    519                raise KeyError 
    520            return width 
    521        except KeyError: 
    522            result = ( 
    523                max(get_cwidth(c.display_text) for c in completion_state.completions) 
    524                + 1 
    525            ) 
    526            self._column_width_for_completion_state[completion_state] = ( 
    527                len(completion_state.completions), 
    528                result, 
    529            ) 
    530            return result 
    531 
    532    def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: 
    533        """ 
    534        Handle scroll and click events. 
    535        """ 
    536        b = get_app().current_buffer 
    537 
    538        def scroll_left() -> None: 
    539            b.complete_previous(count=self._rendered_rows, disable_wrap_around=True) 
    540            self.scroll = max(0, self.scroll - 1) 
    541 
    542        def scroll_right() -> None: 
    543            b.complete_next(count=self._rendered_rows, disable_wrap_around=True) 
    544            self.scroll = min( 
    545                self._total_columns - self._rendered_columns, self.scroll + 1 
    546            ) 
    547 
    548        if mouse_event.event_type == MouseEventType.SCROLL_DOWN: 
    549            scroll_right() 
    550 
    551        elif mouse_event.event_type == MouseEventType.SCROLL_UP: 
    552            scroll_left() 
    553 
    554        elif mouse_event.event_type == MouseEventType.MOUSE_UP: 
    555            x = mouse_event.position.x 
    556            y = mouse_event.position.y 
    557 
    558            # Mouse click on left arrow. 
    559            if x == 0: 
    560                if self._render_left_arrow: 
    561                    scroll_left() 
    562 
    563            # Mouse click on right arrow. 
    564            elif x == self._render_width - 1: 
    565                if self._render_right_arrow: 
    566                    scroll_right() 
    567 
    568            # Mouse click on completion. 
    569            else: 
    570                completion = self._render_pos_to_completion.get((x, y)) 
    571                if completion: 
    572                    b.apply_completion(completion) 
    573 
    574        return None 
    575 
    576    def get_key_bindings(self) -> KeyBindings: 
    577        """ 
    578        Expose key bindings that handle the left/right arrow keys when the menu 
    579        is displayed. 
    580        """ 
    581        from prompt_toolkit.key_binding.key_bindings import KeyBindings 
    582 
    583        kb = KeyBindings() 
    584 
    585        @Condition 
    586        def filter() -> bool: 
    587            "Only handle key bindings if this menu is visible." 
    588            app = get_app() 
    589            complete_state = app.current_buffer.complete_state 
    590 
    591            # There need to be completions, and one needs to be selected. 
    592            if complete_state is None or complete_state.complete_index is None: 
    593                return False 
    594 
    595            # This menu needs to be visible. 
    596            return any(window.content == self for window in app.layout.visible_windows) 
    597 
    598        def move(right: bool = False) -> None: 
    599            buff = get_app().current_buffer 
    600            complete_state = buff.complete_state 
    601 
    602            if complete_state is not None and complete_state.complete_index is not None: 
    603                # Calculate new complete index. 
    604                new_index = complete_state.complete_index 
    605                if right: 
    606                    new_index += self._rendered_rows 
    607                else: 
    608                    new_index -= self._rendered_rows 
    609 
    610                if 0 <= new_index < len(complete_state.completions): 
    611                    buff.go_to_completion(new_index) 
    612 
    613        # NOTE: the is_global is required because the completion menu will 
    614        #       never be focussed. 
    615 
    616        @kb.add("left", is_global=True, filter=filter) 
    617        def _left(event: E) -> None: 
    618            move() 
    619 
    620        @kb.add("right", is_global=True, filter=filter) 
    621        def _right(event: E) -> None: 
    622            move(True) 
    623 
    624        return kb 
    625 
    626 
    627class MultiColumnCompletionsMenu(HSplit): 
    628    """ 
    629    Container that displays the completions in several columns. 
    630    When `show_meta` (a :class:`~prompt_toolkit.filters.Filter`) evaluates 
    631    to True, it shows the meta information at the bottom. 
    632    """ 
    633 
    634    def __init__( 
    635        self, 
    636        min_rows: int = 3, 
    637        suggested_max_column_width: int = 30, 
    638        show_meta: FilterOrBool = True, 
    639        extra_filter: FilterOrBool = True, 
    640        z_index: int = 10**8, 
    641    ) -> None: 
    642        show_meta = to_filter(show_meta) 
    643        extra_filter = to_filter(extra_filter) 
    644 
    645        # Display filter: show when there are completions but not at the point 
    646        # we are returning the input. 
    647        full_filter = extra_filter & has_completions & ~is_done 
    648 
    649        @Condition 
    650        def any_completion_has_meta() -> bool: 
    651            complete_state = get_app().current_buffer.complete_state 
    652            return complete_state is not None and any( 
    653                c.display_meta for c in complete_state.completions 
    654            ) 
    655 
    656        # Create child windows. 
    657        # NOTE: We don't set style='class:completion-menu' to the 
    658        #       `MultiColumnCompletionMenuControl`, because this is used in a 
    659        #       Float that is made transparent, and the size of the control 
    660        #       doesn't always correspond exactly with the size of the 
    661        #       generated content. 
    662        completions_window = ConditionalContainer( 
    663            content=Window( 
    664                content=MultiColumnCompletionMenuControl( 
    665                    min_rows=min_rows, 
    666                    suggested_max_column_width=suggested_max_column_width, 
    667                ), 
    668                width=Dimension(min=8), 
    669                height=Dimension(min=1), 
    670            ), 
    671            filter=full_filter, 
    672        ) 
    673 
    674        meta_window = ConditionalContainer( 
    675            content=Window(content=_SelectedCompletionMetaControl()), 
    676            filter=full_filter & show_meta & any_completion_has_meta, 
    677        ) 
    678 
    679        # Initialize split. 
    680        super().__init__([completions_window, meta_window], z_index=z_index) 
    681 
    682 
    683class _SelectedCompletionMetaControl(UIControl): 
    684    """ 
    685    Control that shows the meta information of the selected completion. 
    686    """ 
    687 
    688    def preferred_width(self, max_available_width: int) -> int | None: 
    689        """ 
    690        Report the width of the longest meta text as the preferred width of this control. 
    691 
    692        It could be that we use less width, but this way, we're sure that the 
    693        layout doesn't change when we select another completion (E.g. that 
    694        completions are suddenly shown in more or fewer columns.) 
    695        """ 
    696        app = get_app() 
    697        if app.current_buffer.complete_state: 
    698            state = app.current_buffer.complete_state 
    699 
    700            if len(state.completions) >= 30: 
    701                # When there are many completions, calling `get_cwidth` for 
    702                # every `display_meta_text` is too expensive. In this case, 
    703                # just return the max available width. There will be enough 
    704                # columns anyway so that the whole screen is filled with 
    705                # completions and `create_content` will then take up as much 
    706                # space as needed. 
    707                return max_available_width 
    708 
    709            return 2 + max( 
    710                get_cwidth(c.display_meta_text) for c in state.completions[:100] 
    711            ) 
    712        else: 
    713            return 0 
    714 
    715    def preferred_height( 
    716        self, 
    717        width: int, 
    718        max_available_height: int, 
    719        wrap_lines: bool, 
    720        get_line_prefix: GetLinePrefixCallable | None, 
    721    ) -> int | None: 
    722        return 1 
    723 
    724    def create_content(self, width: int, height: int) -> UIContent: 
    725        fragments = self._get_text_fragments() 
    726 
    727        def get_line(i: int) -> StyleAndTextTuples: 
    728            return fragments 
    729 
    730        return UIContent(get_line=get_line, line_count=1 if fragments else 0) 
    731 
    732    def _get_text_fragments(self) -> StyleAndTextTuples: 
    733        style = "class:completion-menu.multi-column-meta" 
    734        state = get_app().current_buffer.complete_state 
    735 
    736        if ( 
    737            state 
    738            and state.current_completion 
    739            and state.current_completion.display_meta_text 
    740        ): 
    741            return to_formatted_text( 
    742                cast(StyleAndTextTuples, [("", " ")]) 
    743                + state.current_completion.display_meta 
    744                + [("", " ")], 
    745                style=style, 
    746            ) 
    747 
    748        return []