toolbars.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. from __future__ import annotations
  2. from typing import Any
  3. from prompt_toolkit.application.current import get_app
  4. from prompt_toolkit.buffer import Buffer
  5. from prompt_toolkit.enums import SYSTEM_BUFFER
  6. from prompt_toolkit.filters import (
  7. Condition,
  8. FilterOrBool,
  9. emacs_mode,
  10. has_arg,
  11. has_completions,
  12. has_focus,
  13. has_validation_error,
  14. to_filter,
  15. vi_mode,
  16. vi_navigation_mode,
  17. )
  18. from prompt_toolkit.formatted_text import (
  19. AnyFormattedText,
  20. StyleAndTextTuples,
  21. fragment_list_len,
  22. to_formatted_text,
  23. )
  24. from prompt_toolkit.key_binding.key_bindings import (
  25. ConditionalKeyBindings,
  26. KeyBindings,
  27. KeyBindingsBase,
  28. merge_key_bindings,
  29. )
  30. from prompt_toolkit.key_binding.key_processor import KeyPressEvent
  31. from prompt_toolkit.key_binding.vi_state import InputMode
  32. from prompt_toolkit.keys import Keys
  33. from prompt_toolkit.layout.containers import ConditionalContainer, Container, Window
  34. from prompt_toolkit.layout.controls import (
  35. BufferControl,
  36. FormattedTextControl,
  37. SearchBufferControl,
  38. UIContent,
  39. UIControl,
  40. )
  41. from prompt_toolkit.layout.dimension import Dimension
  42. from prompt_toolkit.layout.processors import BeforeInput
  43. from prompt_toolkit.lexers import SimpleLexer
  44. from prompt_toolkit.search import SearchDirection
  45. __all__ = [
  46. "ArgToolbar",
  47. "CompletionsToolbar",
  48. "FormattedTextToolbar",
  49. "SearchToolbar",
  50. "SystemToolbar",
  51. "ValidationToolbar",
  52. ]
  53. E = KeyPressEvent
  54. class FormattedTextToolbar(Window):
  55. def __init__(self, text: AnyFormattedText, style: str = "", **kw: Any) -> None:
  56. # Note: The style needs to be applied to the toolbar as a whole, not
  57. # just the `FormattedTextControl`.
  58. super().__init__(
  59. FormattedTextControl(text, **kw),
  60. style=style,
  61. dont_extend_height=True,
  62. height=Dimension(min=1),
  63. )
  64. class SystemToolbar:
  65. """
  66. Toolbar for a system prompt.
  67. :param prompt: Prompt to be displayed to the user.
  68. """
  69. def __init__(
  70. self,
  71. prompt: AnyFormattedText = "Shell command: ",
  72. enable_global_bindings: FilterOrBool = True,
  73. ) -> None:
  74. self.prompt = prompt
  75. self.enable_global_bindings = to_filter(enable_global_bindings)
  76. self.system_buffer = Buffer(name=SYSTEM_BUFFER)
  77. self._bindings = self._build_key_bindings()
  78. self.buffer_control = BufferControl(
  79. buffer=self.system_buffer,
  80. lexer=SimpleLexer(style="class:system-toolbar.text"),
  81. input_processors=[
  82. BeforeInput(lambda: self.prompt, style="class:system-toolbar")
  83. ],
  84. key_bindings=self._bindings,
  85. )
  86. self.window = Window(
  87. self.buffer_control, height=1, style="class:system-toolbar"
  88. )
  89. self.container = ConditionalContainer(
  90. content=self.window, filter=has_focus(self.system_buffer)
  91. )
  92. def _get_display_before_text(self) -> StyleAndTextTuples:
  93. return [
  94. ("class:system-toolbar", "Shell command: "),
  95. ("class:system-toolbar.text", self.system_buffer.text),
  96. ("", "\n"),
  97. ]
  98. def _build_key_bindings(self) -> KeyBindingsBase:
  99. focused = has_focus(self.system_buffer)
  100. # Emacs
  101. emacs_bindings = KeyBindings()
  102. handle = emacs_bindings.add
  103. @handle("escape", filter=focused)
  104. @handle("c-g", filter=focused)
  105. @handle("c-c", filter=focused)
  106. def _cancel(event: E) -> None:
  107. "Hide system prompt."
  108. self.system_buffer.reset()
  109. event.app.layout.focus_last()
  110. @handle("enter", filter=focused)
  111. async def _accept(event: E) -> None:
  112. "Run system command."
  113. await event.app.run_system_command(
  114. self.system_buffer.text,
  115. display_before_text=self._get_display_before_text(),
  116. )
  117. self.system_buffer.reset(append_to_history=True)
  118. event.app.layout.focus_last()
  119. # Vi.
  120. vi_bindings = KeyBindings()
  121. handle = vi_bindings.add
  122. @handle("escape", filter=focused)
  123. @handle("c-c", filter=focused)
  124. def _cancel_vi(event: E) -> None:
  125. "Hide system prompt."
  126. event.app.vi_state.input_mode = InputMode.NAVIGATION
  127. self.system_buffer.reset()
  128. event.app.layout.focus_last()
  129. @handle("enter", filter=focused)
  130. async def _accept_vi(event: E) -> None:
  131. "Run system command."
  132. event.app.vi_state.input_mode = InputMode.NAVIGATION
  133. await event.app.run_system_command(
  134. self.system_buffer.text,
  135. display_before_text=self._get_display_before_text(),
  136. )
  137. self.system_buffer.reset(append_to_history=True)
  138. event.app.layout.focus_last()
  139. # Global bindings. (Listen to these bindings, even when this widget is
  140. # not focussed.)
  141. global_bindings = KeyBindings()
  142. handle = global_bindings.add
  143. @handle(Keys.Escape, "!", filter=~focused & emacs_mode, is_global=True)
  144. def _focus_me(event: E) -> None:
  145. "M-'!' will focus this user control."
  146. event.app.layout.focus(self.window)
  147. @handle("!", filter=~focused & vi_mode & vi_navigation_mode, is_global=True)
  148. def _focus_me_vi(event: E) -> None:
  149. "Focus."
  150. event.app.vi_state.input_mode = InputMode.INSERT
  151. event.app.layout.focus(self.window)
  152. return merge_key_bindings(
  153. [
  154. ConditionalKeyBindings(emacs_bindings, emacs_mode),
  155. ConditionalKeyBindings(vi_bindings, vi_mode),
  156. ConditionalKeyBindings(global_bindings, self.enable_global_bindings),
  157. ]
  158. )
  159. def __pt_container__(self) -> Container:
  160. return self.container
  161. class ArgToolbar:
  162. def __init__(self) -> None:
  163. def get_formatted_text() -> StyleAndTextTuples:
  164. arg = get_app().key_processor.arg or ""
  165. if arg == "-":
  166. arg = "-1"
  167. return [
  168. ("class:arg-toolbar", "Repeat: "),
  169. ("class:arg-toolbar.text", arg),
  170. ]
  171. self.window = Window(FormattedTextControl(get_formatted_text), height=1)
  172. self.container = ConditionalContainer(content=self.window, filter=has_arg)
  173. def __pt_container__(self) -> Container:
  174. return self.container
  175. class SearchToolbar:
  176. """
  177. :param vi_mode: Display '/' and '?' instead of I-search.
  178. :param ignore_case: Search case insensitive.
  179. """
  180. def __init__(
  181. self,
  182. search_buffer: Buffer | None = None,
  183. vi_mode: bool = False,
  184. text_if_not_searching: AnyFormattedText = "",
  185. forward_search_prompt: AnyFormattedText = "I-search: ",
  186. backward_search_prompt: AnyFormattedText = "I-search backward: ",
  187. ignore_case: FilterOrBool = False,
  188. ) -> None:
  189. if search_buffer is None:
  190. search_buffer = Buffer()
  191. @Condition
  192. def is_searching() -> bool:
  193. return self.control in get_app().layout.search_links
  194. def get_before_input() -> AnyFormattedText:
  195. if not is_searching():
  196. return text_if_not_searching
  197. elif (
  198. self.control.searcher_search_state.direction == SearchDirection.BACKWARD
  199. ):
  200. return "?" if vi_mode else backward_search_prompt
  201. else:
  202. return "/" if vi_mode else forward_search_prompt
  203. self.search_buffer = search_buffer
  204. self.control = SearchBufferControl(
  205. buffer=search_buffer,
  206. input_processors=[
  207. BeforeInput(get_before_input, style="class:search-toolbar.prompt")
  208. ],
  209. lexer=SimpleLexer(style="class:search-toolbar.text"),
  210. ignore_case=ignore_case,
  211. )
  212. self.container = ConditionalContainer(
  213. content=Window(self.control, height=1, style="class:search-toolbar"),
  214. filter=is_searching,
  215. )
  216. def __pt_container__(self) -> Container:
  217. return self.container
  218. class _CompletionsToolbarControl(UIControl):
  219. def create_content(self, width: int, height: int) -> UIContent:
  220. all_fragments: StyleAndTextTuples = []
  221. complete_state = get_app().current_buffer.complete_state
  222. if complete_state:
  223. completions = complete_state.completions
  224. index = complete_state.complete_index # Can be None!
  225. # Width of the completions without the left/right arrows in the margins.
  226. content_width = width - 6
  227. # Booleans indicating whether we stripped from the left/right
  228. cut_left = False
  229. cut_right = False
  230. # Create Menu content.
  231. fragments: StyleAndTextTuples = []
  232. for i, c in enumerate(completions):
  233. # When there is no more place for the next completion
  234. if fragment_list_len(fragments) + len(c.display_text) >= content_width:
  235. # If the current one was not yet displayed, page to the next sequence.
  236. if i <= (index or 0):
  237. fragments = []
  238. cut_left = True
  239. # If the current one is visible, stop here.
  240. else:
  241. cut_right = True
  242. break
  243. fragments.extend(
  244. to_formatted_text(
  245. c.display_text,
  246. style=(
  247. "class:completion-toolbar.completion.current"
  248. if i == index
  249. else "class:completion-toolbar.completion"
  250. ),
  251. )
  252. )
  253. fragments.append(("", " "))
  254. # Extend/strip until the content width.
  255. fragments.append(("", " " * (content_width - fragment_list_len(fragments))))
  256. fragments = fragments[:content_width]
  257. # Return fragments
  258. all_fragments.append(("", " "))
  259. all_fragments.append(
  260. ("class:completion-toolbar.arrow", "<" if cut_left else " ")
  261. )
  262. all_fragments.append(("", " "))
  263. all_fragments.extend(fragments)
  264. all_fragments.append(("", " "))
  265. all_fragments.append(
  266. ("class:completion-toolbar.arrow", ">" if cut_right else " ")
  267. )
  268. all_fragments.append(("", " "))
  269. def get_line(i: int) -> StyleAndTextTuples:
  270. return all_fragments
  271. return UIContent(get_line=get_line, line_count=1)
  272. class CompletionsToolbar:
  273. def __init__(self) -> None:
  274. self.container = ConditionalContainer(
  275. content=Window(
  276. _CompletionsToolbarControl(), height=1, style="class:completion-toolbar"
  277. ),
  278. filter=has_completions,
  279. )
  280. def __pt_container__(self) -> Container:
  281. return self.container
  282. class ValidationToolbar:
  283. def __init__(self, show_position: bool = False) -> None:
  284. def get_formatted_text() -> StyleAndTextTuples:
  285. buff = get_app().current_buffer
  286. if buff.validation_error:
  287. row, column = buff.document.translate_index_to_position(
  288. buff.validation_error.cursor_position
  289. )
  290. if show_position:
  291. text = f"{buff.validation_error.message} (line={row + 1} column={column + 1})"
  292. else:
  293. text = buff.validation_error.message
  294. return [("class:validation-toolbar", text)]
  295. else:
  296. return []
  297. self.control = FormattedTextControl(get_formatted_text)
  298. self.container = ConditionalContainer(
  299. content=Window(self.control, height=1), filter=has_validation_error
  300. )
  301. def __pt_container__(self) -> Container:
  302. return self.container