""" 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() }