371 lines
12 KiB
Python
371 lines
12 KiB
Python
|
from __future__ import annotations
|
||
|
|
||
|
from typing import Any
|
||
|
|
||
|
from prompt_toolkit.application.current import get_app
|
||
|
from prompt_toolkit.buffer import Buffer
|
||
|
from prompt_toolkit.enums import SYSTEM_BUFFER
|
||
|
from prompt_toolkit.filters import (
|
||
|
Condition,
|
||
|
FilterOrBool,
|
||
|
emacs_mode,
|
||
|
has_arg,
|
||
|
has_completions,
|
||
|
has_focus,
|
||
|
has_validation_error,
|
||
|
to_filter,
|
||
|
vi_mode,
|
||
|
vi_navigation_mode,
|
||
|
)
|
||
|
from prompt_toolkit.formatted_text import (
|
||
|
AnyFormattedText,
|
||
|
StyleAndTextTuples,
|
||
|
fragment_list_len,
|
||
|
to_formatted_text,
|
||
|
)
|
||
|
from prompt_toolkit.key_binding.key_bindings import (
|
||
|
ConditionalKeyBindings,
|
||
|
KeyBindings,
|
||
|
KeyBindingsBase,
|
||
|
merge_key_bindings,
|
||
|
)
|
||
|
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
||
|
from prompt_toolkit.key_binding.vi_state import InputMode
|
||
|
from prompt_toolkit.keys import Keys
|
||
|
from prompt_toolkit.layout.containers import ConditionalContainer, Container, Window
|
||
|
from prompt_toolkit.layout.controls import (
|
||
|
BufferControl,
|
||
|
FormattedTextControl,
|
||
|
SearchBufferControl,
|
||
|
UIContent,
|
||
|
UIControl,
|
||
|
)
|
||
|
from prompt_toolkit.layout.dimension import Dimension
|
||
|
from prompt_toolkit.layout.processors import BeforeInput
|
||
|
from prompt_toolkit.lexers import SimpleLexer
|
||
|
from prompt_toolkit.search import SearchDirection
|
||
|
|
||
|
__all__ = [
|
||
|
"ArgToolbar",
|
||
|
"CompletionsToolbar",
|
||
|
"FormattedTextToolbar",
|
||
|
"SearchToolbar",
|
||
|
"SystemToolbar",
|
||
|
"ValidationToolbar",
|
||
|
]
|
||
|
|
||
|
E = KeyPressEvent
|
||
|
|
||
|
|
||
|
class FormattedTextToolbar(Window):
|
||
|
def __init__(self, text: AnyFormattedText, style: str = "", **kw: Any) -> None:
|
||
|
# Note: The style needs to be applied to the toolbar as a whole, not
|
||
|
# just the `FormattedTextControl`.
|
||
|
super().__init__(
|
||
|
FormattedTextControl(text, **kw),
|
||
|
style=style,
|
||
|
dont_extend_height=True,
|
||
|
height=Dimension(min=1),
|
||
|
)
|
||
|
|
||
|
|
||
|
class SystemToolbar:
|
||
|
"""
|
||
|
Toolbar for a system prompt.
|
||
|
|
||
|
:param prompt: Prompt to be displayed to the user.
|
||
|
"""
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
prompt: AnyFormattedText = "Shell command: ",
|
||
|
enable_global_bindings: FilterOrBool = True,
|
||
|
) -> None:
|
||
|
self.prompt = prompt
|
||
|
self.enable_global_bindings = to_filter(enable_global_bindings)
|
||
|
|
||
|
self.system_buffer = Buffer(name=SYSTEM_BUFFER)
|
||
|
|
||
|
self._bindings = self._build_key_bindings()
|
||
|
|
||
|
self.buffer_control = BufferControl(
|
||
|
buffer=self.system_buffer,
|
||
|
lexer=SimpleLexer(style="class:system-toolbar.text"),
|
||
|
input_processors=[
|
||
|
BeforeInput(lambda: self.prompt, style="class:system-toolbar")
|
||
|
],
|
||
|
key_bindings=self._bindings,
|
||
|
)
|
||
|
|
||
|
self.window = Window(
|
||
|
self.buffer_control, height=1, style="class:system-toolbar"
|
||
|
)
|
||
|
|
||
|
self.container = ConditionalContainer(
|
||
|
content=self.window, filter=has_focus(self.system_buffer)
|
||
|
)
|
||
|
|
||
|
def _get_display_before_text(self) -> StyleAndTextTuples:
|
||
|
return [
|
||
|
("class:system-toolbar", "Shell command: "),
|
||
|
("class:system-toolbar.text", self.system_buffer.text),
|
||
|
("", "\n"),
|
||
|
]
|
||
|
|
||
|
def _build_key_bindings(self) -> KeyBindingsBase:
|
||
|
focused = has_focus(self.system_buffer)
|
||
|
|
||
|
# Emacs
|
||
|
emacs_bindings = KeyBindings()
|
||
|
handle = emacs_bindings.add
|
||
|
|
||
|
@handle("escape", filter=focused)
|
||
|
@handle("c-g", filter=focused)
|
||
|
@handle("c-c", filter=focused)
|
||
|
def _cancel(event: E) -> None:
|
||
|
"Hide system prompt."
|
||
|
self.system_buffer.reset()
|
||
|
event.app.layout.focus_last()
|
||
|
|
||
|
@handle("enter", filter=focused)
|
||
|
async def _accept(event: E) -> None:
|
||
|
"Run system command."
|
||
|
await event.app.run_system_command(
|
||
|
self.system_buffer.text,
|
||
|
display_before_text=self._get_display_before_text(),
|
||
|
)
|
||
|
self.system_buffer.reset(append_to_history=True)
|
||
|
event.app.layout.focus_last()
|
||
|
|
||
|
# Vi.
|
||
|
vi_bindings = KeyBindings()
|
||
|
handle = vi_bindings.add
|
||
|
|
||
|
@handle("escape", filter=focused)
|
||
|
@handle("c-c", filter=focused)
|
||
|
def _cancel_vi(event: E) -> None:
|
||
|
"Hide system prompt."
|
||
|
event.app.vi_state.input_mode = InputMode.NAVIGATION
|
||
|
self.system_buffer.reset()
|
||
|
event.app.layout.focus_last()
|
||
|
|
||
|
@handle("enter", filter=focused)
|
||
|
async def _accept_vi(event: E) -> None:
|
||
|
"Run system command."
|
||
|
event.app.vi_state.input_mode = InputMode.NAVIGATION
|
||
|
await event.app.run_system_command(
|
||
|
self.system_buffer.text,
|
||
|
display_before_text=self._get_display_before_text(),
|
||
|
)
|
||
|
self.system_buffer.reset(append_to_history=True)
|
||
|
event.app.layout.focus_last()
|
||
|
|
||
|
# Global bindings. (Listen to these bindings, even when this widget is
|
||
|
# not focussed.)
|
||
|
global_bindings = KeyBindings()
|
||
|
handle = global_bindings.add
|
||
|
|
||
|
@handle(Keys.Escape, "!", filter=~focused & emacs_mode, is_global=True)
|
||
|
def _focus_me(event: E) -> None:
|
||
|
"M-'!' will focus this user control."
|
||
|
event.app.layout.focus(self.window)
|
||
|
|
||
|
@handle("!", filter=~focused & vi_mode & vi_navigation_mode, is_global=True)
|
||
|
def _focus_me_vi(event: E) -> None:
|
||
|
"Focus."
|
||
|
event.app.vi_state.input_mode = InputMode.INSERT
|
||
|
event.app.layout.focus(self.window)
|
||
|
|
||
|
return merge_key_bindings(
|
||
|
[
|
||
|
ConditionalKeyBindings(emacs_bindings, emacs_mode),
|
||
|
ConditionalKeyBindings(vi_bindings, vi_mode),
|
||
|
ConditionalKeyBindings(global_bindings, self.enable_global_bindings),
|
||
|
]
|
||
|
)
|
||
|
|
||
|
def __pt_container__(self) -> Container:
|
||
|
return self.container
|
||
|
|
||
|
|
||
|
class ArgToolbar:
|
||
|
def __init__(self) -> None:
|
||
|
def get_formatted_text() -> StyleAndTextTuples:
|
||
|
arg = get_app().key_processor.arg or ""
|
||
|
if arg == "-":
|
||
|
arg = "-1"
|
||
|
|
||
|
return [
|
||
|
("class:arg-toolbar", "Repeat: "),
|
||
|
("class:arg-toolbar.text", arg),
|
||
|
]
|
||
|
|
||
|
self.window = Window(FormattedTextControl(get_formatted_text), height=1)
|
||
|
|
||
|
self.container = ConditionalContainer(content=self.window, filter=has_arg)
|
||
|
|
||
|
def __pt_container__(self) -> Container:
|
||
|
return self.container
|
||
|
|
||
|
|
||
|
class SearchToolbar:
|
||
|
"""
|
||
|
:param vi_mode: Display '/' and '?' instead of I-search.
|
||
|
:param ignore_case: Search case insensitive.
|
||
|
"""
|
||
|
|
||
|
def __init__(
|
||
|
self,
|
||
|
search_buffer: Buffer | None = None,
|
||
|
vi_mode: bool = False,
|
||
|
text_if_not_searching: AnyFormattedText = "",
|
||
|
forward_search_prompt: AnyFormattedText = "I-search: ",
|
||
|
backward_search_prompt: AnyFormattedText = "I-search backward: ",
|
||
|
ignore_case: FilterOrBool = False,
|
||
|
) -> None:
|
||
|
if search_buffer is None:
|
||
|
search_buffer = Buffer()
|
||
|
|
||
|
@Condition
|
||
|
def is_searching() -> bool:
|
||
|
return self.control in get_app().layout.search_links
|
||
|
|
||
|
def get_before_input() -> AnyFormattedText:
|
||
|
if not is_searching():
|
||
|
return text_if_not_searching
|
||
|
elif (
|
||
|
self.control.searcher_search_state.direction == SearchDirection.BACKWARD
|
||
|
):
|
||
|
return "?" if vi_mode else backward_search_prompt
|
||
|
else:
|
||
|
return "/" if vi_mode else forward_search_prompt
|
||
|
|
||
|
self.search_buffer = search_buffer
|
||
|
|
||
|
self.control = SearchBufferControl(
|
||
|
buffer=search_buffer,
|
||
|
input_processors=[
|
||
|
BeforeInput(get_before_input, style="class:search-toolbar.prompt")
|
||
|
],
|
||
|
lexer=SimpleLexer(style="class:search-toolbar.text"),
|
||
|
ignore_case=ignore_case,
|
||
|
)
|
||
|
|
||
|
self.container = ConditionalContainer(
|
||
|
content=Window(self.control, height=1, style="class:search-toolbar"),
|
||
|
filter=is_searching,
|
||
|
)
|
||
|
|
||
|
def __pt_container__(self) -> Container:
|
||
|
return self.container
|
||
|
|
||
|
|
||
|
class _CompletionsToolbarControl(UIControl):
|
||
|
def create_content(self, width: int, height: int) -> UIContent:
|
||
|
all_fragments: StyleAndTextTuples = []
|
||
|
|
||
|
complete_state = get_app().current_buffer.complete_state
|
||
|
if complete_state:
|
||
|
completions = complete_state.completions
|
||
|
index = complete_state.complete_index # Can be None!
|
||
|
|
||
|
# Width of the completions without the left/right arrows in the margins.
|
||
|
content_width = width - 6
|
||
|
|
||
|
# Booleans indicating whether we stripped from the left/right
|
||
|
cut_left = False
|
||
|
cut_right = False
|
||
|
|
||
|
# Create Menu content.
|
||
|
fragments: StyleAndTextTuples = []
|
||
|
|
||
|
for i, c in enumerate(completions):
|
||
|
# When there is no more place for the next completion
|
||
|
if fragment_list_len(fragments) + len(c.display_text) >= content_width:
|
||
|
# If the current one was not yet displayed, page to the next sequence.
|
||
|
if i <= (index or 0):
|
||
|
fragments = []
|
||
|
cut_left = True
|
||
|
# If the current one is visible, stop here.
|
||
|
else:
|
||
|
cut_right = True
|
||
|
break
|
||
|
|
||
|
fragments.extend(
|
||
|
to_formatted_text(
|
||
|
c.display_text,
|
||
|
style=(
|
||
|
"class:completion-toolbar.completion.current"
|
||
|
if i == index
|
||
|
else "class:completion-toolbar.completion"
|
||
|
),
|
||
|
)
|
||
|
)
|
||
|
fragments.append(("", " "))
|
||
|
|
||
|
# Extend/strip until the content width.
|
||
|
fragments.append(("", " " * (content_width - fragment_list_len(fragments))))
|
||
|
fragments = fragments[:content_width]
|
||
|
|
||
|
# Return fragments
|
||
|
all_fragments.append(("", " "))
|
||
|
all_fragments.append(
|
||
|
("class:completion-toolbar.arrow", "<" if cut_left else " ")
|
||
|
)
|
||
|
all_fragments.append(("", " "))
|
||
|
|
||
|
all_fragments.extend(fragments)
|
||
|
|
||
|
all_fragments.append(("", " "))
|
||
|
all_fragments.append(
|
||
|
("class:completion-toolbar.arrow", ">" if cut_right else " ")
|
||
|
)
|
||
|
all_fragments.append(("", " "))
|
||
|
|
||
|
def get_line(i: int) -> StyleAndTextTuples:
|
||
|
return all_fragments
|
||
|
|
||
|
return UIContent(get_line=get_line, line_count=1)
|
||
|
|
||
|
|
||
|
class CompletionsToolbar:
|
||
|
def __init__(self) -> None:
|
||
|
self.container = ConditionalContainer(
|
||
|
content=Window(
|
||
|
_CompletionsToolbarControl(), height=1, style="class:completion-toolbar"
|
||
|
),
|
||
|
filter=has_completions,
|
||
|
)
|
||
|
|
||
|
def __pt_container__(self) -> Container:
|
||
|
return self.container
|
||
|
|
||
|
|
||
|
class ValidationToolbar:
|
||
|
def __init__(self, show_position: bool = False) -> None:
|
||
|
def get_formatted_text() -> StyleAndTextTuples:
|
||
|
buff = get_app().current_buffer
|
||
|
|
||
|
if buff.validation_error:
|
||
|
row, column = buff.document.translate_index_to_position(
|
||
|
buff.validation_error.cursor_position
|
||
|
)
|
||
|
|
||
|
if show_position:
|
||
|
text = f"{buff.validation_error.message} (line={row + 1} column={column + 1})"
|
||
|
else:
|
||
|
text = buff.validation_error.message
|
||
|
|
||
|
return [("class:validation-toolbar", text)]
|
||
|
else:
|
||
|
return []
|
||
|
|
||
|
self.control = FormattedTextControl(get_formatted_text)
|
||
|
|
||
|
self.container = ConditionalContainer(
|
||
|
content=Window(self.control, height=1), filter=has_validation_error
|
||
|
)
|
||
|
|
||
|
def __pt_container__(self) -> Container:
|
||
|
return self.container
|