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.2.2, created at 2023-03-26 06:07 +0000

1from __future__ import annotations 

2 

3import math 

4from itertools import zip_longest 

5from typing import ( 

6 TYPE_CHECKING, 

7 Callable, 

8 Dict, 

9 Iterable, 

10 Optional, 

11 Sequence, 

12 Tuple, 

13 TypeVar, 

14 Union, 

15 cast, 

16) 

17from weakref import WeakKeyDictionary 

18 

19from prompt_toolkit.application.current import get_app 

20from prompt_toolkit.buffer import CompletionState 

21from prompt_toolkit.completion import Completion 

22from prompt_toolkit.data_structures import Point 

23from prompt_toolkit.filters import ( 

24 Condition, 

25 FilterOrBool, 

26 has_completions, 

27 is_done, 

28 to_filter, 

29) 

30from prompt_toolkit.formatted_text import ( 

31 StyleAndTextTuples, 

32 fragment_list_width, 

33 to_formatted_text, 

34) 

35from prompt_toolkit.key_binding.key_processor import KeyPressEvent 

36from prompt_toolkit.layout.utils import explode_text_fragments 

37from prompt_toolkit.mouse_events import MouseEvent, MouseEventType 

38from prompt_toolkit.utils import get_cwidth 

39 

40from .containers import ConditionalContainer, HSplit, ScrollOffsets, Window 

41from .controls import GetLinePrefixCallable, UIContent, UIControl 

42from .dimension import Dimension 

43from .margins import ScrollbarMargin 

44 

45if TYPE_CHECKING: 

46 from prompt_toolkit.key_binding.key_bindings import ( 

47 KeyBindings, 

48 NotImplementedOrNone, 

49 ) 

50 

51 

52__all__ = [ 

53 "CompletionsMenu", 

54 "MultiColumnCompletionsMenu", 

55] 

56 

57E = KeyPressEvent 

58 

59 

60class CompletionsMenuControl(UIControl): 

61 """ 

62 Helper for drawing the complete menu to the screen. 

63 

64 :param scroll_offset: Number (integer) representing the preferred amount of 

65 completions to be displayed before and after the current one. When this 

66 is a very high number, the current completion will be shown in the 

67 middle most of the time. 

68 """ 

69 

70 # Preferred minimum size of the menu control. 

71 # The CompletionsMenu class defines a width of 8, and there is a scrollbar 

72 # of 1.) 

73 MIN_WIDTH = 7 

74 

75 def has_focus(self) -> bool: 

76 return False 

77 

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

79 complete_state = get_app().current_buffer.complete_state 

80 if complete_state: 

81 menu_width = self._get_menu_width(500, complete_state) 

82 menu_meta_width = self._get_menu_meta_width(500, complete_state) 

83 

84 return menu_width + menu_meta_width 

85 else: 

86 return 0 

87 

88 def preferred_height( 

89 self, 

90 width: int, 

91 max_available_height: int, 

92 wrap_lines: bool, 

93 get_line_prefix: GetLinePrefixCallable | None, 

94 ) -> int | None: 

95 complete_state = get_app().current_buffer.complete_state 

96 if complete_state: 

97 return len(complete_state.completions) 

98 else: 

99 return 0 

100 

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

102 """ 

103 Create a UIContent object for this control. 

104 """ 

105 complete_state = get_app().current_buffer.complete_state 

106 if complete_state: 

107 completions = complete_state.completions 

108 index = complete_state.complete_index # Can be None! 

109 

110 # Calculate width of completions menu. 

111 menu_width = self._get_menu_width(width, complete_state) 

112 menu_meta_width = self._get_menu_meta_width( 

113 width - menu_width, complete_state 

114 ) 

115 show_meta = self._show_meta(complete_state) 

116 

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

118 c = completions[i] 

119 is_current_completion = i == index 

120 result = _get_menu_item_fragments( 

121 c, is_current_completion, menu_width, space_after=True 

122 ) 

123 

124 if show_meta: 

125 result += self._get_menu_item_meta_fragments( 

126 c, is_current_completion, menu_meta_width 

127 ) 

128 return result 

129 

130 return UIContent( 

131 get_line=get_line, 

132 cursor_position=Point(x=0, y=index or 0), 

133 line_count=len(completions), 

134 ) 

135 

136 return UIContent() 

137 

138 def _show_meta(self, complete_state: CompletionState) -> bool: 

139 """ 

140 Return ``True`` if we need to show a column with meta information. 

141 """ 

142 return any(c.display_meta_text for c in complete_state.completions) 

143 

144 def _get_menu_width(self, max_width: int, complete_state: CompletionState) -> int: 

145 """ 

146 Return the width of the main column. 

147 """ 

148 return min( 

149 max_width, 

150 max( 

151 self.MIN_WIDTH, 

152 max(get_cwidth(c.display_text) for c in complete_state.completions) + 2, 

153 ), 

154 ) 

155 

156 def _get_menu_meta_width( 

157 self, max_width: int, complete_state: CompletionState 

158 ) -> int: 

159 """ 

160 Return the width of the meta column. 

161 """ 

162 

163 def meta_width(completion: Completion) -> int: 

164 return get_cwidth(completion.display_meta_text) 

165 

166 if self._show_meta(complete_state): 

167 # If the amount of completions is over 200, compute the width based 

168 # on the first 200 completions, otherwise this can be very slow. 

169 completions = complete_state.completions 

170 if len(completions) > 200: 

171 completions = completions[:200] 

172 

173 return min(max_width, max(meta_width(c) for c in completions) + 2) 

174 else: 

175 return 0 

176 

177 def _get_menu_item_meta_fragments( 

178 self, completion: Completion, is_current_completion: bool, width: int 

179 ) -> StyleAndTextTuples: 

180 if is_current_completion: 

181 style_str = "class:completion-menu.meta.completion.current" 

182 else: 

183 style_str = "class:completion-menu.meta.completion" 

184 

185 text, tw = _trim_formatted_text(completion.display_meta, width - 2) 

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

187 

188 return to_formatted_text( 

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

190 style=style_str, 

191 ) 

192 

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

194 """ 

195 Handle mouse events: clicking and scrolling. 

196 """ 

197 b = get_app().current_buffer 

198 

199 if mouse_event.event_type == MouseEventType.MOUSE_UP: 

200 # Select completion. 

201 b.go_to_completion(mouse_event.position.y) 

202 b.complete_state = None 

203 

204 elif mouse_event.event_type == MouseEventType.SCROLL_DOWN: 

205 # Scroll up. 

206 b.complete_next(count=3, disable_wrap_around=True) 

207 

208 elif mouse_event.event_type == MouseEventType.SCROLL_UP: 

209 # Scroll down. 

210 b.complete_previous(count=3, disable_wrap_around=True) 

211 

212 return None 

213 

214 

215def _get_menu_item_fragments( 

216 completion: Completion, 

217 is_current_completion: bool, 

218 width: int, 

219 space_after: bool = False, 

220) -> StyleAndTextTuples: 

221 """ 

222 Get the style/text tuples for a menu item, styled and trimmed to the given 

223 width. 

224 """ 

225 if is_current_completion: 

226 style_str = "class:completion-menu.completion.current {} {}".format( 

227 completion.style, 

228 completion.selected_style, 

229 ) 

230 else: 

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

232 

233 text, tw = _trim_formatted_text( 

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

235 ) 

236 

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

238 

239 return to_formatted_text( 

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

241 style=style_str, 

242 ) 

243 

244 

245def _trim_formatted_text( 

246 formatted_text: StyleAndTextTuples, max_width: int 

247) -> tuple[StyleAndTextTuples, int]: 

248 """ 

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

250 Returns (text, width) tuple. 

251 """ 

252 width = fragment_list_width(formatted_text) 

253 

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

255 if width > max_width: 

256 result = [] # Text fragments. 

257 remaining_width = max_width - 3 

258 

259 for style_and_ch in explode_text_fragments(formatted_text): 

260 ch_width = get_cwidth(style_and_ch[1]) 

261 

262 if ch_width <= remaining_width: 

263 result.append(style_and_ch) 

264 remaining_width -= ch_width 

265 else: 

266 break 

267 

268 result.append(("", "...")) 

269 

270 return result, max_width - remaining_width 

271 else: 

272 return formatted_text, width 

273 

274 

275class CompletionsMenu(ConditionalContainer): 

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

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

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

279 def __init__( 

280 self, 

281 max_height: int | None = None, 

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

283 extra_filter: FilterOrBool = True, 

284 display_arrows: FilterOrBool = False, 

285 z_index: int = 10**8, 

286 ) -> None: 

287 extra_filter = to_filter(extra_filter) 

288 display_arrows = to_filter(display_arrows) 

289 

290 super().__init__( 

291 content=Window( 

292 content=CompletionsMenuControl(), 

293 width=Dimension(min=8), 

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

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

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

297 dont_extend_width=True, 

298 style="class:completion-menu", 

299 z_index=z_index, 

300 ), 

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

302 # returning the input. 

303 filter=has_completions & ~is_done & extra_filter, 

304 ) 

305 

306 

307class MultiColumnCompletionMenuControl(UIControl): 

308 """ 

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

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

311 arrow is shown on the left or right side. 

312 

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

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

315 rows until this value is reached. 

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

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

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

319 This results in less completions displayed and additional scrolling. 

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

321 widths are calculated, then the heights.) 

322 

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

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

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

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

327 reduce the amount of columns. 

328 """ 

329 

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

331 

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

333 assert min_rows >= 1 

334 

335 self.min_rows = min_rows 

336 self.suggested_max_column_width = suggested_max_column_width 

337 self.scroll = 0 

338 

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

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

341 # navigates through the completions. 

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

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

344 # `CompletionState` while loading.) 

345 self._column_width_for_completion_state: WeakKeyDictionary[ 

346 CompletionState, Tuple[int, int] 

347 ] = WeakKeyDictionary() 

348 

349 # Info of last rendering. 

350 self._rendered_rows = 0 

351 self._rendered_columns = 0 

352 self._total_columns = 0 

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

354 self._render_left_arrow = False 

355 self._render_right_arrow = False 

356 self._render_width = 0 

357 

358 def reset(self) -> None: 

359 self.scroll = 0 

360 

361 def has_focus(self) -> bool: 

362 return False 

363 

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

365 """ 

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

367 as possible horizontally. 

368 """ 

369 complete_state = get_app().current_buffer.complete_state 

370 if complete_state is None: 

371 return 0 

372 

373 column_width = self._get_column_width(complete_state) 

374 result = int( 

375 column_width 

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

377 ) 

378 

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

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

381 # width. 

382 while ( 

383 result > column_width 

384 and result > max_available_width - self._required_margin 

385 ): 

386 result -= column_width 

387 return result + self._required_margin 

388 

389 def preferred_height( 

390 self, 

391 width: int, 

392 max_available_height: int, 

393 wrap_lines: bool, 

394 get_line_prefix: GetLinePrefixCallable | None, 

395 ) -> int | None: 

396 """ 

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

398 """ 

399 complete_state = get_app().current_buffer.complete_state 

400 if complete_state is None: 

401 return 0 

402 

403 column_width = self._get_column_width(complete_state) 

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

405 

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

407 

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

409 """ 

410 Create a UIContent object for this menu. 

411 """ 

412 complete_state = get_app().current_buffer.complete_state 

413 if complete_state is None: 

414 return UIContent() 

415 

416 column_width = self._get_column_width(complete_state) 

417 self._render_pos_to_completion = {} 

418 

419 _T = TypeVar("_T") 

420 

421 def grouper( 

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

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

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

425 args = [iter(iterable)] * n 

426 return zip_longest(fillvalue=fillvalue, *args) 

427 

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

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

430 return ( 

431 complete_state is not None 

432 and complete_state.complete_index is not None 

433 and c == complete_state.current_completion 

434 ) 

435 

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

437 # left and right arrow. 

438 HORIZONTAL_MARGIN_REQUIRED = 3 

439 

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

441 # the available width. 

442 column_width = min(width - HORIZONTAL_MARGIN_REQUIRED, column_width) 

443 

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

445 # some very wide entries, shrink it anyway. 

446 if column_width > self.suggested_max_column_width: 

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

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

449 column_width //= column_width // self.suggested_max_column_width 

450 

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

452 

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

454 rows_ = list(zip(*columns_)) 

455 

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

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

458 self.scroll = min( 

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

460 ) 

461 

462 render_left_arrow = self.scroll > 0 

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

464 

465 # Write completions to screen. 

466 fragments_for_line = [] 

467 

468 for row_index, row in enumerate(rows_): 

469 fragments: StyleAndTextTuples = [] 

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

471 

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

473 if render_left_arrow: 

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

475 elif render_right_arrow: 

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

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

478 fragments.append(("", " ")) 

479 

480 # Draw row content. 

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

482 if c is not None: 

483 fragments += _get_menu_item_fragments( 

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

485 ) 

486 

487 # Remember render position for mouse click handler. 

488 for x in range(column_width): 

489 self._render_pos_to_completion[ 

490 (column_index * column_width + x, row_index) 

491 ] = c 

492 else: 

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

494 

495 # Draw trailing padding for this row. 

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

497 if render_left_arrow or render_right_arrow: 

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

499 

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

501 if render_right_arrow: 

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

503 elif render_left_arrow: 

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

505 

506 # Add line. 

507 fragments_for_line.append( 

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

509 ) 

510 

511 self._rendered_rows = height 

512 self._rendered_columns = visible_columns 

513 self._total_columns = len(columns_) 

514 self._render_left_arrow = render_left_arrow 

515 self._render_right_arrow = render_right_arrow 

516 self._render_width = ( 

517 column_width * visible_columns + render_left_arrow + render_right_arrow + 1 

518 ) 

519 

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

521 return fragments_for_line[i] 

522 

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

524 

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

526 """ 

527 Return the width of each column. 

528 """ 

529 try: 

530 count, width = self._column_width_for_completion_state[completion_state] 

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

532 # Number of completions changed, recompute. 

533 raise KeyError 

534 return width 

535 except KeyError: 

536 result = ( 

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

538 + 1 

539 ) 

540 self._column_width_for_completion_state[completion_state] = ( 

541 len(completion_state.completions), 

542 result, 

543 ) 

544 return result 

545 

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

547 """ 

548 Handle scroll and click events. 

549 """ 

550 b = get_app().current_buffer 

551 

552 def scroll_left() -> None: 

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

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

555 

556 def scroll_right() -> None: 

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

558 self.scroll = min( 

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

560 ) 

561 

562 if mouse_event.event_type == MouseEventType.SCROLL_DOWN: 

563 scroll_right() 

564 

565 elif mouse_event.event_type == MouseEventType.SCROLL_UP: 

566 scroll_left() 

567 

568 elif mouse_event.event_type == MouseEventType.MOUSE_UP: 

569 x = mouse_event.position.x 

570 y = mouse_event.position.y 

571 

572 # Mouse click on left arrow. 

573 if x == 0: 

574 if self._render_left_arrow: 

575 scroll_left() 

576 

577 # Mouse click on right arrow. 

578 elif x == self._render_width - 1: 

579 if self._render_right_arrow: 

580 scroll_right() 

581 

582 # Mouse click on completion. 

583 else: 

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

585 if completion: 

586 b.apply_completion(completion) 

587 

588 return None 

589 

590 def get_key_bindings(self) -> KeyBindings: 

591 """ 

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

593 is displayed. 

594 """ 

595 from prompt_toolkit.key_binding.key_bindings import KeyBindings 

596 

597 kb = KeyBindings() 

598 

599 @Condition 

600 def filter() -> bool: 

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

602 app = get_app() 

603 complete_state = app.current_buffer.complete_state 

604 

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

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

607 return False 

608 

609 # This menu needs to be visible. 

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

611 

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

613 buff = get_app().current_buffer 

614 complete_state = buff.complete_state 

615 

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

617 # Calculate new complete index. 

618 new_index = complete_state.complete_index 

619 if right: 

620 new_index += self._rendered_rows 

621 else: 

622 new_index -= self._rendered_rows 

623 

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

625 buff.go_to_completion(new_index) 

626 

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

628 # never be focussed. 

629 

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

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

632 move() 

633 

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

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

636 move(True) 

637 

638 return kb 

639 

640 

641class MultiColumnCompletionsMenu(HSplit): 

642 """ 

643 Container that displays the completions in several columns. 

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

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

646 """ 

647 

648 def __init__( 

649 self, 

650 min_rows: int = 3, 

651 suggested_max_column_width: int = 30, 

652 show_meta: FilterOrBool = True, 

653 extra_filter: FilterOrBool = True, 

654 z_index: int = 10**8, 

655 ) -> None: 

656 show_meta = to_filter(show_meta) 

657 extra_filter = to_filter(extra_filter) 

658 

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

660 # we are returning the input. 

661 full_filter = has_completions & ~is_done & extra_filter 

662 

663 @Condition 

664 def any_completion_has_meta() -> bool: 

665 complete_state = get_app().current_buffer.complete_state 

666 return complete_state is not None and any( 

667 c.display_meta for c in complete_state.completions 

668 ) 

669 

670 # Create child windows. 

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

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

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

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

675 # generated content. 

676 completions_window = ConditionalContainer( 

677 content=Window( 

678 content=MultiColumnCompletionMenuControl( 

679 min_rows=min_rows, 

680 suggested_max_column_width=suggested_max_column_width, 

681 ), 

682 width=Dimension(min=8), 

683 height=Dimension(min=1), 

684 ), 

685 filter=full_filter, 

686 ) 

687 

688 meta_window = ConditionalContainer( 

689 content=Window(content=_SelectedCompletionMetaControl()), 

690 filter=show_meta & full_filter & any_completion_has_meta, 

691 ) 

692 

693 # Initialise split. 

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

695 

696 

697class _SelectedCompletionMetaControl(UIControl): 

698 """ 

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

700 """ 

701 

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

703 """ 

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

705 

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

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

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

709 """ 

710 app = get_app() 

711 if app.current_buffer.complete_state: 

712 state = app.current_buffer.complete_state 

713 

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

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

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

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

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

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

720 # space as needed. 

721 return max_available_width 

722 

723 return 2 + max( 

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

725 ) 

726 else: 

727 return 0 

728 

729 def preferred_height( 

730 self, 

731 width: int, 

732 max_available_height: int, 

733 wrap_lines: bool, 

734 get_line_prefix: GetLinePrefixCallable | None, 

735 ) -> int | None: 

736 return 1 

737 

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

739 fragments = self._get_text_fragments() 

740 

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

742 return fragments 

743 

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

745 

746 def _get_text_fragments(self) -> StyleAndTextTuples: 

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

748 state = get_app().current_buffer.complete_state 

749 

750 if ( 

751 state 

752 and state.current_completion 

753 and state.current_completion.display_meta_text 

754 ): 

755 return to_formatted_text( 

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

757 + state.current_completion.display_meta 

758 + [("", " ")], 

759 style=style, 

760 ) 

761 

762 return []