Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/prompt_toolkit/layout/menus.py: 19%

296 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 06:09 +0000

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 = "class:completion-menu.completion.current {} {}".format( 

216 completion.style, 

217 completion.selected_style, 

218 ) 

219 else: 

220 style_str = "class:completion-menu.completion " + completion.style 

221 

222 text, tw = _trim_formatted_text( 

223 completion.display, (width - 2 if space_after else width - 1) 

224 ) 

225 

226 padding = " " * (width - 1 - tw) 

227 

228 return to_formatted_text( 

229 cast(StyleAndTextTuples, []) + [("", " ")] + text + [("", padding)], 

230 style=style_str, 

231 ) 

232 

233 

234def _trim_formatted_text( 

235 formatted_text: StyleAndTextTuples, max_width: int 

236) -> tuple[StyleAndTextTuples, int]: 

237 """ 

238 Trim the text to `max_width`, append dots when the text is too long. 

239 Returns (text, width) tuple. 

240 """ 

241 width = fragment_list_width(formatted_text) 

242 

243 # When the text is too wide, trim it. 

244 if width > max_width: 

245 result = [] # Text fragments. 

246 remaining_width = max_width - 3 

247 

248 for style_and_ch in explode_text_fragments(formatted_text): 

249 ch_width = get_cwidth(style_and_ch[1]) 

250 

251 if ch_width <= remaining_width: 

252 result.append(style_and_ch) 

253 remaining_width -= ch_width 

254 else: 

255 break 

256 

257 result.append(("", "...")) 

258 

259 return result, max_width - remaining_width 

260 else: 

261 return formatted_text, width 

262 

263 

264class CompletionsMenu(ConditionalContainer): 

265 # NOTE: We use a pretty big z_index by default. Menus are supposed to be 

266 # above anything else. We also want to make sure that the content is 

267 # visible at the point where we draw this menu. 

268 def __init__( 

269 self, 

270 max_height: int | None = None, 

271 scroll_offset: int | Callable[[], int] = 0, 

272 extra_filter: FilterOrBool = True, 

273 display_arrows: FilterOrBool = False, 

274 z_index: int = 10**8, 

275 ) -> None: 

276 extra_filter = to_filter(extra_filter) 

277 display_arrows = to_filter(display_arrows) 

278 

279 super().__init__( 

280 content=Window( 

281 content=CompletionsMenuControl(), 

282 width=Dimension(min=8), 

283 height=Dimension(min=1, max=max_height), 

284 scroll_offsets=ScrollOffsets(top=scroll_offset, bottom=scroll_offset), 

285 right_margins=[ScrollbarMargin(display_arrows=display_arrows)], 

286 dont_extend_width=True, 

287 style="class:completion-menu", 

288 z_index=z_index, 

289 ), 

290 # Show when there are completions but not at the point we are 

291 # returning the input. 

292 filter=extra_filter & has_completions & ~is_done, 

293 ) 

294 

295 

296class MultiColumnCompletionMenuControl(UIControl): 

297 """ 

298 Completion menu that displays all the completions in several columns. 

299 When there are more completions than space for them to be displayed, an 

300 arrow is shown on the left or right side. 

301 

302 `min_rows` indicates how many rows will be available in any possible case. 

303 When this is larger than one, it will try to use less columns and more 

304 rows until this value is reached. 

305 Be careful passing in a too big value, if less than the given amount of 

306 rows are available, more columns would have been required, but 

307 `preferred_width` doesn't know about that and reports a too small value. 

308 This results in less completions displayed and additional scrolling. 

309 (It's a limitation of how the layout engine currently works: first the 

310 widths are calculated, then the heights.) 

311 

312 :param suggested_max_column_width: The suggested max width of a column. 

313 The column can still be bigger than this, but if there is place for two 

314 columns of this width, we will display two columns. This to avoid that 

315 if there is one very wide completion, that it doesn't significantly 

316 reduce the amount of columns. 

317 """ 

318 

319 _required_margin = 3 # One extra padding on the right + space for arrows. 

320 

321 def __init__(self, min_rows: int = 3, suggested_max_column_width: int = 30) -> None: 

322 assert min_rows >= 1 

323 

324 self.min_rows = min_rows 

325 self.suggested_max_column_width = suggested_max_column_width 

326 self.scroll = 0 

327 

328 # Cache for column width computations. This computation is not cheap, 

329 # so we don't want to do it over and over again while the user 

330 # navigates through the completions. 

331 # (map `completion_state` to `(completion_count, width)`. We remember 

332 # the count, because a completer can add new completions to the 

333 # `CompletionState` while loading.) 

334 self._column_width_for_completion_state: WeakKeyDictionary[ 

335 CompletionState, tuple[int, int] 

336 ] = WeakKeyDictionary() 

337 

338 # Info of last rendering. 

339 self._rendered_rows = 0 

340 self._rendered_columns = 0 

341 self._total_columns = 0 

342 self._render_pos_to_completion: dict[tuple[int, int], Completion] = {} 

343 self._render_left_arrow = False 

344 self._render_right_arrow = False 

345 self._render_width = 0 

346 

347 def reset(self) -> None: 

348 self.scroll = 0 

349 

350 def has_focus(self) -> bool: 

351 return False 

352 

353 def preferred_width(self, max_available_width: int) -> int | None: 

354 """ 

355 Preferred width: prefer to use at least min_rows, but otherwise as much 

356 as possible horizontally. 

357 """ 

358 complete_state = get_app().current_buffer.complete_state 

359 if complete_state is None: 

360 return 0 

361 

362 column_width = self._get_column_width(complete_state) 

363 result = int( 

364 column_width 

365 * math.ceil(len(complete_state.completions) / float(self.min_rows)) 

366 ) 

367 

368 # When the desired width is still more than the maximum available, 

369 # reduce by removing columns until we are less than the available 

370 # width. 

371 while ( 

372 result > column_width 

373 and result > max_available_width - self._required_margin 

374 ): 

375 result -= column_width 

376 return result + self._required_margin 

377 

378 def preferred_height( 

379 self, 

380 width: int, 

381 max_available_height: int, 

382 wrap_lines: bool, 

383 get_line_prefix: GetLinePrefixCallable | None, 

384 ) -> int | None: 

385 """ 

386 Preferred height: as much as needed in order to display all the completions. 

387 """ 

388 complete_state = get_app().current_buffer.complete_state 

389 if complete_state is None: 

390 return 0 

391 

392 column_width = self._get_column_width(complete_state) 

393 column_count = max(1, (width - self._required_margin) // column_width) 

394 

395 return int(math.ceil(len(complete_state.completions) / float(column_count))) 

396 

397 def create_content(self, width: int, height: int) -> UIContent: 

398 """ 

399 Create a UIContent object for this menu. 

400 """ 

401 complete_state = get_app().current_buffer.complete_state 

402 if complete_state is None: 

403 return UIContent() 

404 

405 column_width = self._get_column_width(complete_state) 

406 self._render_pos_to_completion = {} 

407 

408 _T = TypeVar("_T") 

409 

410 def grouper( 

411 n: int, iterable: Iterable[_T], fillvalue: _T | None = None 

412 ) -> Iterable[Sequence[_T | None]]: 

413 "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx" 

414 args = [iter(iterable)] * n 

415 return zip_longest(fillvalue=fillvalue, *args) 

416 

417 def is_current_completion(completion: Completion) -> bool: 

418 "Returns True when this completion is the currently selected one." 

419 return ( 

420 complete_state is not None 

421 and complete_state.complete_index is not None 

422 and c == complete_state.current_completion 

423 ) 

424 

425 # Space required outside of the regular columns, for displaying the 

426 # left and right arrow. 

427 HORIZONTAL_MARGIN_REQUIRED = 3 

428 

429 # There should be at least one column, but it cannot be wider than 

430 # the available width. 

431 column_width = min(width - HORIZONTAL_MARGIN_REQUIRED, column_width) 

432 

433 # However, when the columns tend to be very wide, because there are 

434 # some very wide entries, shrink it anyway. 

435 if column_width > self.suggested_max_column_width: 

436 # `column_width` can still be bigger that `suggested_max_column_width`, 

437 # but if there is place for two columns, we divide by two. 

438 column_width //= column_width // self.suggested_max_column_width 

439 

440 visible_columns = max(1, (width - self._required_margin) // column_width) 

441 

442 columns_ = list(grouper(height, complete_state.completions)) 

443 rows_ = list(zip(*columns_)) 

444 

445 # Make sure the current completion is always visible: update scroll offset. 

446 selected_column = (complete_state.complete_index or 0) // height 

447 self.scroll = min( 

448 selected_column, max(self.scroll, selected_column - visible_columns + 1) 

449 ) 

450 

451 render_left_arrow = self.scroll > 0 

452 render_right_arrow = self.scroll < len(rows_[0]) - visible_columns 

453 

454 # Write completions to screen. 

455 fragments_for_line = [] 

456 

457 for row_index, row in enumerate(rows_): 

458 fragments: StyleAndTextTuples = [] 

459 middle_row = row_index == len(rows_) // 2 

460 

461 # Draw left arrow if we have hidden completions on the left. 

462 if render_left_arrow: 

463 fragments.append(("class:scrollbar", "<" if middle_row else " ")) 

464 elif render_right_arrow: 

465 # Reserve one column empty space. (If there is a right 

466 # arrow right now, there can be a left arrow as well.) 

467 fragments.append(("", " ")) 

468 

469 # Draw row content. 

470 for column_index, c in enumerate(row[self.scroll :][:visible_columns]): 

471 if c is not None: 

472 fragments += _get_menu_item_fragments( 

473 c, is_current_completion(c), column_width, space_after=False 

474 ) 

475 

476 # Remember render position for mouse click handler. 

477 for x in range(column_width): 

478 self._render_pos_to_completion[ 

479 (column_index * column_width + x, row_index) 

480 ] = c 

481 else: 

482 fragments.append(("class:completion", " " * column_width)) 

483 

484 # Draw trailing padding for this row. 

485 # (_get_menu_item_fragments only returns padding on the left.) 

486 if render_left_arrow or render_right_arrow: 

487 fragments.append(("class:completion", " ")) 

488 

489 # Draw right arrow if we have hidden completions on the right. 

490 if render_right_arrow: 

491 fragments.append(("class:scrollbar", ">" if middle_row else " ")) 

492 elif render_left_arrow: 

493 fragments.append(("class:completion", " ")) 

494 

495 # Add line. 

496 fragments_for_line.append( 

497 to_formatted_text(fragments, style="class:completion-menu") 

498 ) 

499 

500 self._rendered_rows = height 

501 self._rendered_columns = visible_columns 

502 self._total_columns = len(columns_) 

503 self._render_left_arrow = render_left_arrow 

504 self._render_right_arrow = render_right_arrow 

505 self._render_width = ( 

506 column_width * visible_columns + render_left_arrow + render_right_arrow + 1 

507 ) 

508 

509 def get_line(i: int) -> StyleAndTextTuples: 

510 return fragments_for_line[i] 

511 

512 return UIContent(get_line=get_line, line_count=len(rows_)) 

513 

514 def _get_column_width(self, completion_state: CompletionState) -> int: 

515 """ 

516 Return the width of each column. 

517 """ 

518 try: 

519 count, width = self._column_width_for_completion_state[completion_state] 

520 if count != len(completion_state.completions): 

521 # Number of completions changed, recompute. 

522 raise KeyError 

523 return width 

524 except KeyError: 

525 result = ( 

526 max(get_cwidth(c.display_text) for c in completion_state.completions) 

527 + 1 

528 ) 

529 self._column_width_for_completion_state[completion_state] = ( 

530 len(completion_state.completions), 

531 result, 

532 ) 

533 return result 

534 

535 def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: 

536 """ 

537 Handle scroll and click events. 

538 """ 

539 b = get_app().current_buffer 

540 

541 def scroll_left() -> None: 

542 b.complete_previous(count=self._rendered_rows, disable_wrap_around=True) 

543 self.scroll = max(0, self.scroll - 1) 

544 

545 def scroll_right() -> None: 

546 b.complete_next(count=self._rendered_rows, disable_wrap_around=True) 

547 self.scroll = min( 

548 self._total_columns - self._rendered_columns, self.scroll + 1 

549 ) 

550 

551 if mouse_event.event_type == MouseEventType.SCROLL_DOWN: 

552 scroll_right() 

553 

554 elif mouse_event.event_type == MouseEventType.SCROLL_UP: 

555 scroll_left() 

556 

557 elif mouse_event.event_type == MouseEventType.MOUSE_UP: 

558 x = mouse_event.position.x 

559 y = mouse_event.position.y 

560 

561 # Mouse click on left arrow. 

562 if x == 0: 

563 if self._render_left_arrow: 

564 scroll_left() 

565 

566 # Mouse click on right arrow. 

567 elif x == self._render_width - 1: 

568 if self._render_right_arrow: 

569 scroll_right() 

570 

571 # Mouse click on completion. 

572 else: 

573 completion = self._render_pos_to_completion.get((x, y)) 

574 if completion: 

575 b.apply_completion(completion) 

576 

577 return None 

578 

579 def get_key_bindings(self) -> KeyBindings: 

580 """ 

581 Expose key bindings that handle the left/right arrow keys when the menu 

582 is displayed. 

583 """ 

584 from prompt_toolkit.key_binding.key_bindings import KeyBindings 

585 

586 kb = KeyBindings() 

587 

588 @Condition 

589 def filter() -> bool: 

590 "Only handle key bindings if this menu is visible." 

591 app = get_app() 

592 complete_state = app.current_buffer.complete_state 

593 

594 # There need to be completions, and one needs to be selected. 

595 if complete_state is None or complete_state.complete_index is None: 

596 return False 

597 

598 # This menu needs to be visible. 

599 return any(window.content == self for window in app.layout.visible_windows) 

600 

601 def move(right: bool = False) -> None: 

602 buff = get_app().current_buffer 

603 complete_state = buff.complete_state 

604 

605 if complete_state is not None and complete_state.complete_index is not None: 

606 # Calculate new complete index. 

607 new_index = complete_state.complete_index 

608 if right: 

609 new_index += self._rendered_rows 

610 else: 

611 new_index -= self._rendered_rows 

612 

613 if 0 <= new_index < len(complete_state.completions): 

614 buff.go_to_completion(new_index) 

615 

616 # NOTE: the is_global is required because the completion menu will 

617 # never be focussed. 

618 

619 @kb.add("left", is_global=True, filter=filter) 

620 def _left(event: E) -> None: 

621 move() 

622 

623 @kb.add("right", is_global=True, filter=filter) 

624 def _right(event: E) -> None: 

625 move(True) 

626 

627 return kb 

628 

629 

630class MultiColumnCompletionsMenu(HSplit): 

631 """ 

632 Container that displays the completions in several columns. 

633 When `show_meta` (a :class:`~prompt_toolkit.filters.Filter`) evaluates 

634 to True, it shows the meta information at the bottom. 

635 """ 

636 

637 def __init__( 

638 self, 

639 min_rows: int = 3, 

640 suggested_max_column_width: int = 30, 

641 show_meta: FilterOrBool = True, 

642 extra_filter: FilterOrBool = True, 

643 z_index: int = 10**8, 

644 ) -> None: 

645 show_meta = to_filter(show_meta) 

646 extra_filter = to_filter(extra_filter) 

647 

648 # Display filter: show when there are completions but not at the point 

649 # we are returning the input. 

650 full_filter = extra_filter & has_completions & ~is_done 

651 

652 @Condition 

653 def any_completion_has_meta() -> bool: 

654 complete_state = get_app().current_buffer.complete_state 

655 return complete_state is not None and any( 

656 c.display_meta for c in complete_state.completions 

657 ) 

658 

659 # Create child windows. 

660 # NOTE: We don't set style='class:completion-menu' to the 

661 # `MultiColumnCompletionMenuControl`, because this is used in a 

662 # Float that is made transparent, and the size of the control 

663 # doesn't always correspond exactly with the size of the 

664 # generated content. 

665 completions_window = ConditionalContainer( 

666 content=Window( 

667 content=MultiColumnCompletionMenuControl( 

668 min_rows=min_rows, 

669 suggested_max_column_width=suggested_max_column_width, 

670 ), 

671 width=Dimension(min=8), 

672 height=Dimension(min=1), 

673 ), 

674 filter=full_filter, 

675 ) 

676 

677 meta_window = ConditionalContainer( 

678 content=Window(content=_SelectedCompletionMetaControl()), 

679 filter=full_filter & show_meta & any_completion_has_meta, 

680 ) 

681 

682 # Initialize split. 

683 super().__init__([completions_window, meta_window], z_index=z_index) 

684 

685 

686class _SelectedCompletionMetaControl(UIControl): 

687 """ 

688 Control that shows the meta information of the selected completion. 

689 """ 

690 

691 def preferred_width(self, max_available_width: int) -> int | None: 

692 """ 

693 Report the width of the longest meta text as the preferred width of this control. 

694 

695 It could be that we use less width, but this way, we're sure that the 

696 layout doesn't change when we select another completion (E.g. that 

697 completions are suddenly shown in more or fewer columns.) 

698 """ 

699 app = get_app() 

700 if app.current_buffer.complete_state: 

701 state = app.current_buffer.complete_state 

702 

703 if len(state.completions) >= 30: 

704 # When there are many completions, calling `get_cwidth` for 

705 # every `display_meta_text` is too expensive. In this case, 

706 # just return the max available width. There will be enough 

707 # columns anyway so that the whole screen is filled with 

708 # completions and `create_content` will then take up as much 

709 # space as needed. 

710 return max_available_width 

711 

712 return 2 + max( 

713 get_cwidth(c.display_meta_text) for c in state.completions[:100] 

714 ) 

715 else: 

716 return 0 

717 

718 def preferred_height( 

719 self, 

720 width: int, 

721 max_available_height: int, 

722 wrap_lines: bool, 

723 get_line_prefix: GetLinePrefixCallable | None, 

724 ) -> int | None: 

725 return 1 

726 

727 def create_content(self, width: int, height: int) -> UIContent: 

728 fragments = self._get_text_fragments() 

729 

730 def get_line(i: int) -> StyleAndTextTuples: 

731 return fragments 

732 

733 return UIContent(get_line=get_line, line_count=1 if fragments else 0) 

734 

735 def _get_text_fragments(self) -> StyleAndTextTuples: 

736 style = "class:completion-menu.multi-column-meta" 

737 state = get_app().current_buffer.complete_state 

738 

739 if ( 

740 state 

741 and state.current_completion 

742 and state.current_completion.display_meta_text 

743 ): 

744 return to_formatted_text( 

745 cast(StyleAndTextTuples, [("", " ")]) 

746 + state.current_completion.display_meta 

747 + [("", " ")], 

748 style=style, 

749 ) 

750 

751 return []