204 lines
6.7 KiB
Python
204 lines
6.7 KiB
Python
|
"""
|
||
|
Key binding handlers for displaying completions.
|
||
|
"""
|
||
|
import asyncio
|
||
|
import math
|
||
|
from typing import TYPE_CHECKING, List
|
||
|
|
||
|
from prompt_toolkit.application.run_in_terminal import in_terminal
|
||
|
from prompt_toolkit.completion import (
|
||
|
CompleteEvent,
|
||
|
Completion,
|
||
|
get_common_complete_suffix,
|
||
|
)
|
||
|
from prompt_toolkit.formatted_text import StyleAndTextTuples
|
||
|
from prompt_toolkit.key_binding.key_bindings import KeyBindings
|
||
|
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
||
|
from prompt_toolkit.keys import Keys
|
||
|
from prompt_toolkit.utils import get_cwidth
|
||
|
|
||
|
if TYPE_CHECKING:
|
||
|
from prompt_toolkit.application import Application
|
||
|
from prompt_toolkit.shortcuts import PromptSession
|
||
|
|
||
|
__all__ = [
|
||
|
"generate_completions",
|
||
|
"display_completions_like_readline",
|
||
|
]
|
||
|
|
||
|
E = KeyPressEvent
|
||
|
|
||
|
|
||
|
def generate_completions(event: E) -> None:
|
||
|
r"""
|
||
|
Tab-completion: where the first tab completes the common suffix and the
|
||
|
second tab lists all the completions.
|
||
|
"""
|
||
|
b = event.current_buffer
|
||
|
|
||
|
# When already navigating through completions, select the next one.
|
||
|
if b.complete_state:
|
||
|
b.complete_next()
|
||
|
else:
|
||
|
b.start_completion(insert_common_part=True)
|
||
|
|
||
|
|
||
|
def display_completions_like_readline(event: E) -> None:
|
||
|
"""
|
||
|
Key binding handler for readline-style tab completion.
|
||
|
This is meant to be as similar as possible to the way how readline displays
|
||
|
completions.
|
||
|
|
||
|
Generate the completions immediately (blocking) and display them above the
|
||
|
prompt in columns.
|
||
|
|
||
|
Usage::
|
||
|
|
||
|
# Call this handler when 'Tab' has been pressed.
|
||
|
key_bindings.add(Keys.ControlI)(display_completions_like_readline)
|
||
|
"""
|
||
|
# Request completions.
|
||
|
b = event.current_buffer
|
||
|
if b.completer is None:
|
||
|
return
|
||
|
complete_event = CompleteEvent(completion_requested=True)
|
||
|
completions = list(b.completer.get_completions(b.document, complete_event))
|
||
|
|
||
|
# Calculate the common suffix.
|
||
|
common_suffix = get_common_complete_suffix(b.document, completions)
|
||
|
|
||
|
# One completion: insert it.
|
||
|
if len(completions) == 1:
|
||
|
b.delete_before_cursor(-completions[0].start_position)
|
||
|
b.insert_text(completions[0].text)
|
||
|
# Multiple completions with common part.
|
||
|
elif common_suffix:
|
||
|
b.insert_text(common_suffix)
|
||
|
# Otherwise: display all completions.
|
||
|
elif completions:
|
||
|
_display_completions_like_readline(event.app, completions)
|
||
|
|
||
|
|
||
|
def _display_completions_like_readline(
|
||
|
app: "Application", completions: List[Completion]
|
||
|
) -> "asyncio.Task[None]":
|
||
|
"""
|
||
|
Display the list of completions in columns above the prompt.
|
||
|
This will ask for a confirmation if there are too many completions to fit
|
||
|
on a single page and provide a paginator to walk through them.
|
||
|
"""
|
||
|
from prompt_toolkit.formatted_text import to_formatted_text
|
||
|
from prompt_toolkit.shortcuts.prompt import create_confirm_session
|
||
|
|
||
|
# Get terminal dimensions.
|
||
|
term_size = app.output.get_size()
|
||
|
term_width = term_size.columns
|
||
|
term_height = term_size.rows
|
||
|
|
||
|
# Calculate amount of required columns/rows for displaying the
|
||
|
# completions. (Keep in mind that completions are displayed
|
||
|
# alphabetically column-wise.)
|
||
|
max_compl_width = min(
|
||
|
term_width, max(get_cwidth(c.display_text) for c in completions) + 1
|
||
|
)
|
||
|
column_count = max(1, term_width // max_compl_width)
|
||
|
completions_per_page = column_count * (term_height - 1)
|
||
|
page_count = int(math.ceil(len(completions) / float(completions_per_page)))
|
||
|
# Note: math.ceil can return float on Python2.
|
||
|
|
||
|
def display(page: int) -> None:
|
||
|
# Display completions.
|
||
|
page_completions = completions[
|
||
|
page * completions_per_page : (page + 1) * completions_per_page
|
||
|
]
|
||
|
|
||
|
page_row_count = int(math.ceil(len(page_completions) / float(column_count)))
|
||
|
page_columns = [
|
||
|
page_completions[i * page_row_count : (i + 1) * page_row_count]
|
||
|
for i in range(column_count)
|
||
|
]
|
||
|
|
||
|
result: StyleAndTextTuples = []
|
||
|
|
||
|
for r in range(page_row_count):
|
||
|
for c in range(column_count):
|
||
|
try:
|
||
|
completion = page_columns[c][r]
|
||
|
style = "class:readline-like-completions.completion " + (
|
||
|
completion.style or ""
|
||
|
)
|
||
|
|
||
|
result.extend(to_formatted_text(completion.display, style=style))
|
||
|
|
||
|
# Add padding.
|
||
|
padding = max_compl_width - get_cwidth(completion.display_text)
|
||
|
result.append((completion.style, " " * padding))
|
||
|
except IndexError:
|
||
|
pass
|
||
|
result.append(("", "\n"))
|
||
|
|
||
|
app.print_text(to_formatted_text(result, "class:readline-like-completions"))
|
||
|
|
||
|
# User interaction through an application generator function.
|
||
|
async def run_compl() -> None:
|
||
|
" Coroutine. "
|
||
|
async with in_terminal(render_cli_done=True):
|
||
|
if len(completions) > completions_per_page:
|
||
|
# Ask confirmation if it doesn't fit on the screen.
|
||
|
confirm = await create_confirm_session(
|
||
|
"Display all {} possibilities?".format(len(completions)),
|
||
|
).prompt_async()
|
||
|
|
||
|
if confirm:
|
||
|
# Display pages.
|
||
|
for page in range(page_count):
|
||
|
display(page)
|
||
|
|
||
|
if page != page_count - 1:
|
||
|
# Display --MORE-- and go to the next page.
|
||
|
show_more = await _create_more_session(
|
||
|
"--MORE--"
|
||
|
).prompt_async()
|
||
|
|
||
|
if not show_more:
|
||
|
return
|
||
|
else:
|
||
|
app.output.flush()
|
||
|
else:
|
||
|
# Display all completions.
|
||
|
display(0)
|
||
|
|
||
|
return app.create_background_task(run_compl())
|
||
|
|
||
|
|
||
|
def _create_more_session(message: str = "--MORE--") -> "PromptSession":
|
||
|
"""
|
||
|
Create a `PromptSession` object for displaying the "--MORE--".
|
||
|
"""
|
||
|
from prompt_toolkit.shortcuts import PromptSession
|
||
|
|
||
|
bindings = KeyBindings()
|
||
|
|
||
|
@bindings.add(" ")
|
||
|
@bindings.add("y")
|
||
|
@bindings.add("Y")
|
||
|
@bindings.add(Keys.ControlJ)
|
||
|
@bindings.add(Keys.ControlM)
|
||
|
@bindings.add(Keys.ControlI) # Tab.
|
||
|
def _yes(event: E) -> None:
|
||
|
event.app.exit(result=True)
|
||
|
|
||
|
@bindings.add("n")
|
||
|
@bindings.add("N")
|
||
|
@bindings.add("q")
|
||
|
@bindings.add("Q")
|
||
|
@bindings.add(Keys.ControlC)
|
||
|
def _no(event: E) -> None:
|
||
|
event.app.exit(result=False)
|
||
|
|
||
|
@bindings.add(Keys.Any)
|
||
|
def _ignore(event: E) -> None:
|
||
|
" Disable inserting of text. "
|
||
|
|
||
|
return PromptSession(message, key_bindings=bindings, erase_when_done=True)
|