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