search.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. """
  2. Search operations.
  3. For the key bindings implementation with attached filters, check
  4. `prompt_toolkit.key_binding.bindings.search`. (Use these for new key bindings
  5. instead of calling these function directly.)
  6. """
  7. from __future__ import annotations
  8. from enum import Enum
  9. from typing import TYPE_CHECKING
  10. from .application.current import get_app
  11. from .filters import FilterOrBool, is_searching, to_filter
  12. from .key_binding.vi_state import InputMode
  13. if TYPE_CHECKING:
  14. from prompt_toolkit.layout.controls import BufferControl, SearchBufferControl
  15. from prompt_toolkit.layout.layout import Layout
  16. __all__ = [
  17. "SearchDirection",
  18. "start_search",
  19. "stop_search",
  20. ]
  21. class SearchDirection(Enum):
  22. FORWARD = "FORWARD"
  23. BACKWARD = "BACKWARD"
  24. class SearchState:
  25. """
  26. A search 'query', associated with a search field (like a SearchToolbar).
  27. Every searchable `BufferControl` points to a `search_buffer_control`
  28. (another `BufferControls`) which represents the search field. The
  29. `SearchState` attached to that search field is used for storing the current
  30. search query.
  31. It is possible to have one searchfield for multiple `BufferControls`. In
  32. that case, they'll share the same `SearchState`.
  33. If there are multiple `BufferControls` that display the same `Buffer`, then
  34. they can have a different `SearchState` each (if they have a different
  35. search control).
  36. """
  37. __slots__ = ("text", "direction", "ignore_case")
  38. def __init__(
  39. self,
  40. text: str = "",
  41. direction: SearchDirection = SearchDirection.FORWARD,
  42. ignore_case: FilterOrBool = False,
  43. ) -> None:
  44. self.text = text
  45. self.direction = direction
  46. self.ignore_case = to_filter(ignore_case)
  47. def __repr__(self) -> str:
  48. return "{}({!r}, direction={!r}, ignore_case={!r})".format(
  49. self.__class__.__name__,
  50. self.text,
  51. self.direction,
  52. self.ignore_case,
  53. )
  54. def __invert__(self) -> SearchState:
  55. """
  56. Create a new SearchState where backwards becomes forwards and the other
  57. way around.
  58. """
  59. if self.direction == SearchDirection.BACKWARD:
  60. direction = SearchDirection.FORWARD
  61. else:
  62. direction = SearchDirection.BACKWARD
  63. return SearchState(
  64. text=self.text, direction=direction, ignore_case=self.ignore_case
  65. )
  66. def start_search(
  67. buffer_control: BufferControl | None = None,
  68. direction: SearchDirection = SearchDirection.FORWARD,
  69. ) -> None:
  70. """
  71. Start search through the given `buffer_control` using the
  72. `search_buffer_control`.
  73. :param buffer_control: Start search for this `BufferControl`. If not given,
  74. search through the current control.
  75. """
  76. from prompt_toolkit.layout.controls import BufferControl
  77. assert buffer_control is None or isinstance(buffer_control, BufferControl)
  78. layout = get_app().layout
  79. # When no control is given, use the current control if that's a BufferControl.
  80. if buffer_control is None:
  81. if not isinstance(layout.current_control, BufferControl):
  82. return
  83. buffer_control = layout.current_control
  84. # Only if this control is searchable.
  85. search_buffer_control = buffer_control.search_buffer_control
  86. if search_buffer_control:
  87. buffer_control.search_state.direction = direction
  88. # Make sure to focus the search BufferControl
  89. layout.focus(search_buffer_control)
  90. # Remember search link.
  91. layout.search_links[search_buffer_control] = buffer_control
  92. # If we're in Vi mode, make sure to go into insert mode.
  93. get_app().vi_state.input_mode = InputMode.INSERT
  94. def stop_search(buffer_control: BufferControl | None = None) -> None:
  95. """
  96. Stop search through the given `buffer_control`.
  97. """
  98. layout = get_app().layout
  99. if buffer_control is None:
  100. buffer_control = layout.search_target_buffer_control
  101. if buffer_control is None:
  102. # (Should not happen, but possible when `stop_search` is called
  103. # when we're not searching.)
  104. return
  105. search_buffer_control = buffer_control.search_buffer_control
  106. else:
  107. assert buffer_control in layout.search_links.values()
  108. search_buffer_control = _get_reverse_search_links(layout)[buffer_control]
  109. # Focus the original buffer again.
  110. layout.focus(buffer_control)
  111. if search_buffer_control is not None:
  112. # Remove the search link.
  113. del layout.search_links[search_buffer_control]
  114. # Reset content of search control.
  115. search_buffer_control.buffer.reset()
  116. # If we're in Vi mode, go back to navigation mode.
  117. get_app().vi_state.input_mode = InputMode.NAVIGATION
  118. def do_incremental_search(direction: SearchDirection, count: int = 1) -> None:
  119. """
  120. Apply search, but keep search buffer focused.
  121. """
  122. assert is_searching()
  123. layout = get_app().layout
  124. # Only search if the current control is a `BufferControl`.
  125. from prompt_toolkit.layout.controls import BufferControl
  126. search_control = layout.current_control
  127. if not isinstance(search_control, BufferControl):
  128. return
  129. prev_control = layout.search_target_buffer_control
  130. if prev_control is None:
  131. return
  132. search_state = prev_control.search_state
  133. # Update search_state.
  134. direction_changed = search_state.direction != direction
  135. search_state.text = search_control.buffer.text
  136. search_state.direction = direction
  137. # Apply search to current buffer.
  138. if not direction_changed:
  139. prev_control.buffer.apply_search(
  140. search_state, include_current_position=False, count=count
  141. )
  142. def accept_search() -> None:
  143. """
  144. Accept current search query. Focus original `BufferControl` again.
  145. """
  146. layout = get_app().layout
  147. search_control = layout.current_control
  148. target_buffer_control = layout.search_target_buffer_control
  149. from prompt_toolkit.layout.controls import BufferControl
  150. if not isinstance(search_control, BufferControl):
  151. return
  152. if target_buffer_control is None:
  153. return
  154. search_state = target_buffer_control.search_state
  155. # Update search state.
  156. if search_control.buffer.text:
  157. search_state.text = search_control.buffer.text
  158. # Apply search.
  159. target_buffer_control.buffer.apply_search(
  160. search_state, include_current_position=True
  161. )
  162. # Add query to history of search line.
  163. search_control.buffer.append_to_history()
  164. # Stop search and focus previous control again.
  165. stop_search(target_buffer_control)
  166. def _get_reverse_search_links(
  167. layout: Layout,
  168. ) -> dict[BufferControl, SearchBufferControl]:
  169. """
  170. Return mapping from BufferControl to SearchBufferControl.
  171. """
  172. return {
  173. buffer_control: search_buffer_control
  174. for search_buffer_control, buffer_control in layout.search_links.items()
  175. }