menus.py 27 KB

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