summaryrefslogtreecommitdiffstats
path: root/src/prompt_toolkit/search.py
blob: fd90a04e901341a01ac3754963ccf42793e1eabf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
"""
Search operations.

For the key bindings implementation with attached filters, check
`prompt_toolkit.key_binding.bindings.search`. (Use these for new key bindings
instead of calling these function directly.)
"""
from __future__ import annotations

from enum import Enum
from typing import TYPE_CHECKING

from .application.current import get_app
from .filters import FilterOrBool, is_searching, to_filter
from .key_binding.vi_state import InputMode

if TYPE_CHECKING:
    from prompt_toolkit.layout.controls import BufferControl, SearchBufferControl
    from prompt_toolkit.layout.layout import Layout

__all__ = [
    "SearchDirection",
    "start_search",
    "stop_search",
]


class SearchDirection(Enum):
    FORWARD = "FORWARD"
    BACKWARD = "BACKWARD"


class SearchState:
    """
    A search 'query', associated with a search field (like a SearchToolbar).

    Every searchable `BufferControl` points to a `search_buffer_control`
    (another `BufferControls`) which represents the search field. The
    `SearchState` attached to that search field is used for storing the current
    search query.

    It is possible to have one searchfield for multiple `BufferControls`. In
    that case, they'll share the same `SearchState`.
    If there are multiple `BufferControls` that display the same `Buffer`, then
    they can have a different `SearchState` each (if they have a different
    search control).
    """

    __slots__ = ("text", "direction", "ignore_case")

    def __init__(
        self,
        text: str = "",
        direction: SearchDirection = SearchDirection.FORWARD,
        ignore_case: FilterOrBool = False,
    ) -> None:
        self.text = text
        self.direction = direction
        self.ignore_case = to_filter(ignore_case)

    def __repr__(self) -> str:
        return "{}({!r}, direction={!r}, ignore_case={!r})".format(
            self.__class__.__name__,
            self.text,
            self.direction,
            self.ignore_case,
        )

    def __invert__(self) -> SearchState:
        """
        Create a new SearchState where backwards becomes forwards and the other
        way around.
        """
        if self.direction == SearchDirection.BACKWARD:
            direction = SearchDirection.FORWARD
        else:
            direction = SearchDirection.BACKWARD

        return SearchState(
            text=self.text, direction=direction, ignore_case=self.ignore_case
        )


def start_search(
    buffer_control: BufferControl | None = None,
    direction: SearchDirection = SearchDirection.FORWARD,
) -> None:
    """
    Start search through the given `buffer_control` using the
    `search_buffer_control`.

    :param buffer_control: Start search for this `BufferControl`. If not given,
        search through the current control.
    """
    from prompt_toolkit.layout.controls import BufferControl

    assert buffer_control is None or isinstance(buffer_control, BufferControl)

    layout = get_app().layout

    # When no control is given, use the current control if that's a BufferControl.
    if buffer_control is None:
        if not isinstance(layout.current_control, BufferControl):
            return
        buffer_control = layout.current_control

    # Only if this control is searchable.
    search_buffer_control = buffer_control.search_buffer_control

    if search_buffer_control:
        buffer_control.search_state.direction = direction

        # Make sure to focus the search BufferControl
        layout.focus(search_buffer_control)

        # Remember search link.
        layout.search_links[search_buffer_control] = buffer_control

        # If we're in Vi mode, make sure to go into insert mode.
        get_app().vi_state.input_mode = InputMode.INSERT


def stop_search(buffer_control: BufferControl | None = None) -> None:
    """
    Stop search through the given `buffer_control`.
    """
    layout = get_app().layout

    if buffer_control is None:
        buffer_control = layout.search_target_buffer_control
        if buffer_control is None:
            # (Should not happen, but possible when `stop_search` is called
            # when we're not searching.)
            return
        search_buffer_control = buffer_control.search_buffer_control
    else:
        assert buffer_control in layout.search_links.values()
        search_buffer_control = _get_reverse_search_links(layout)[buffer_control]

    # Focus the original buffer again.
    layout.focus(buffer_control)

    if search_buffer_control is not None:
        # Remove the search link.
        del layout.search_links[search_buffer_control]

        # Reset content of search control.
        search_buffer_control.buffer.reset()

    # If we're in Vi mode, go back to navigation mode.
    get_app().vi_state.input_mode = InputMode.NAVIGATION


def do_incremental_search(direction: SearchDirection, count: int = 1) -> None:
    """
    Apply search, but keep search buffer focused.
    """
    assert is_searching()

    layout = get_app().layout

    # Only search if the current control is a `BufferControl`.
    from prompt_toolkit.layout.controls import BufferControl

    search_control = layout.current_control
    if not isinstance(search_control, BufferControl):
        return

    prev_control = layout.search_target_buffer_control
    if prev_control is None:
        return
    search_state = prev_control.search_state

    # Update search_state.
    direction_changed = search_state.direction != direction

    search_state.text = search_control.buffer.text
    search_state.direction = direction

    # Apply search to current buffer.
    if not direction_changed:
        prev_control.buffer.apply_search(
            search_state, include_current_position=False, count=count
        )


def accept_search() -> None:
    """
    Accept current search query. Focus original `BufferControl` again.
    """
    layout = get_app().layout

    search_control = layout.current_control
    target_buffer_control = layout.search_target_buffer_control

    from prompt_toolkit.layout.controls import BufferControl

    if not isinstance(search_control, BufferControl):
        return
    if target_buffer_control is None:
        return

    search_state = target_buffer_control.search_state

    # Update search state.
    if search_control.buffer.text:
        search_state.text = search_control.buffer.text

    # Apply search.
    target_buffer_control.buffer.apply_search(
        search_state, include_current_position=True
    )

    # Add query to history of search line.
    search_control.buffer.append_to_history()

    # Stop search and focus previous control again.
    stop_search(target_buffer_control)


def _get_reverse_search_links(
    layout: Layout,
) -> dict[BufferControl, SearchBufferControl]:
    """
    Return mapping from BufferControl to SearchBufferControl.
    """
    return {
        buffer_control: search_buffer_control
        for search_buffer_control, buffer_control in layout.search_links.items()
    }