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