completion.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. """
  2. Key binding handlers for displaying completions.
  3. """
  4. from __future__ import annotations
  5. import asyncio
  6. import math
  7. from typing import TYPE_CHECKING
  8. from prompt_toolkit.application.run_in_terminal import in_terminal
  9. from prompt_toolkit.completion import (
  10. CompleteEvent,
  11. Completion,
  12. get_common_complete_suffix,
  13. )
  14. from prompt_toolkit.formatted_text import StyleAndTextTuples
  15. from prompt_toolkit.key_binding.key_bindings import KeyBindings
  16. from prompt_toolkit.key_binding.key_processor import KeyPressEvent
  17. from prompt_toolkit.keys import Keys
  18. from prompt_toolkit.utils import get_cwidth
  19. if TYPE_CHECKING:
  20. from prompt_toolkit.application import Application
  21. from prompt_toolkit.shortcuts import PromptSession
  22. __all__ = [
  23. "generate_completions",
  24. "display_completions_like_readline",
  25. ]
  26. E = KeyPressEvent
  27. def generate_completions(event: E) -> None:
  28. r"""
  29. Tab-completion: where the first tab completes the common suffix and the
  30. second tab lists all the completions.
  31. """
  32. b = event.current_buffer
  33. # When already navigating through completions, select the next one.
  34. if b.complete_state:
  35. b.complete_next()
  36. else:
  37. b.start_completion(insert_common_part=True)
  38. def display_completions_like_readline(event: E) -> None:
  39. """
  40. Key binding handler for readline-style tab completion.
  41. This is meant to be as similar as possible to the way how readline displays
  42. completions.
  43. Generate the completions immediately (blocking) and display them above the
  44. prompt in columns.
  45. Usage::
  46. # Call this handler when 'Tab' has been pressed.
  47. key_bindings.add(Keys.ControlI)(display_completions_like_readline)
  48. """
  49. # Request completions.
  50. b = event.current_buffer
  51. if b.completer is None:
  52. return
  53. complete_event = CompleteEvent(completion_requested=True)
  54. completions = list(b.completer.get_completions(b.document, complete_event))
  55. # Calculate the common suffix.
  56. common_suffix = get_common_complete_suffix(b.document, completions)
  57. # One completion: insert it.
  58. if len(completions) == 1:
  59. b.delete_before_cursor(-completions[0].start_position)
  60. b.insert_text(completions[0].text)
  61. # Multiple completions with common part.
  62. elif common_suffix:
  63. b.insert_text(common_suffix)
  64. # Otherwise: display all completions.
  65. elif completions:
  66. _display_completions_like_readline(event.app, completions)
  67. def _display_completions_like_readline(
  68. app: Application[object], completions: list[Completion]
  69. ) -> asyncio.Task[None]:
  70. """
  71. Display the list of completions in columns above the prompt.
  72. This will ask for a confirmation if there are too many completions to fit
  73. on a single page and provide a paginator to walk through them.
  74. """
  75. from prompt_toolkit.formatted_text import to_formatted_text
  76. from prompt_toolkit.shortcuts.prompt import create_confirm_session
  77. # Get terminal dimensions.
  78. term_size = app.output.get_size()
  79. term_width = term_size.columns
  80. term_height = term_size.rows
  81. # Calculate amount of required columns/rows for displaying the
  82. # completions. (Keep in mind that completions are displayed
  83. # alphabetically column-wise.)
  84. max_compl_width = min(
  85. term_width, max(get_cwidth(c.display_text) for c in completions) + 1
  86. )
  87. column_count = max(1, term_width // max_compl_width)
  88. completions_per_page = column_count * (term_height - 1)
  89. page_count = int(math.ceil(len(completions) / float(completions_per_page)))
  90. # Note: math.ceil can return float on Python2.
  91. def display(page: int) -> None:
  92. # Display completions.
  93. page_completions = completions[
  94. page * completions_per_page : (page + 1) * completions_per_page
  95. ]
  96. page_row_count = int(math.ceil(len(page_completions) / float(column_count)))
  97. page_columns = [
  98. page_completions[i * page_row_count : (i + 1) * page_row_count]
  99. for i in range(column_count)
  100. ]
  101. result: StyleAndTextTuples = []
  102. for r in range(page_row_count):
  103. for c in range(column_count):
  104. try:
  105. completion = page_columns[c][r]
  106. style = "class:readline-like-completions.completion " + (
  107. completion.style or ""
  108. )
  109. result.extend(to_formatted_text(completion.display, style=style))
  110. # Add padding.
  111. padding = max_compl_width - get_cwidth(completion.display_text)
  112. result.append((completion.style, " " * padding))
  113. except IndexError:
  114. pass
  115. result.append(("", "\n"))
  116. app.print_text(to_formatted_text(result, "class:readline-like-completions"))
  117. # User interaction through an application generator function.
  118. async def run_compl() -> None:
  119. "Coroutine."
  120. async with in_terminal(render_cli_done=True):
  121. if len(completions) > completions_per_page:
  122. # Ask confirmation if it doesn't fit on the screen.
  123. confirm = await create_confirm_session(
  124. f"Display all {len(completions)} possibilities?",
  125. ).prompt_async()
  126. if confirm:
  127. # Display pages.
  128. for page in range(page_count):
  129. display(page)
  130. if page != page_count - 1:
  131. # Display --MORE-- and go to the next page.
  132. show_more = await _create_more_session(
  133. "--MORE--"
  134. ).prompt_async()
  135. if not show_more:
  136. return
  137. else:
  138. app.output.flush()
  139. else:
  140. # Display all completions.
  141. display(0)
  142. return app.create_background_task(run_compl())
  143. def _create_more_session(message: str = "--MORE--") -> PromptSession[bool]:
  144. """
  145. Create a `PromptSession` object for displaying the "--MORE--".
  146. """
  147. from prompt_toolkit.shortcuts import PromptSession
  148. bindings = KeyBindings()
  149. @bindings.add(" ")
  150. @bindings.add("y")
  151. @bindings.add("Y")
  152. @bindings.add(Keys.ControlJ)
  153. @bindings.add(Keys.ControlM)
  154. @bindings.add(Keys.ControlI) # Tab.
  155. def _yes(event: E) -> None:
  156. event.app.exit(result=True)
  157. @bindings.add("n")
  158. @bindings.add("N")
  159. @bindings.add("q")
  160. @bindings.add("Q")
  161. @bindings.add(Keys.ControlC)
  162. def _no(event: E) -> None:
  163. event.app.exit(result=False)
  164. @bindings.add(Keys.Any)
  165. def _ignore(event: E) -> None:
  166. "Disable inserting of text."
  167. return PromptSession(message, key_bindings=bindings, erase_when_done=True)