Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/prompt_toolkit/layout/controls.py: 24%
327 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:05 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:05 +0000
1"""
2User interface Controls for the layout.
3"""
4from __future__ import annotations
6import time
7from abc import ABCMeta, abstractmethod
8from typing import TYPE_CHECKING, Callable, Hashable, Iterable, NamedTuple
10from prompt_toolkit.application.current import get_app
11from prompt_toolkit.buffer import Buffer
12from prompt_toolkit.cache import SimpleCache
13from prompt_toolkit.data_structures import Point
14from prompt_toolkit.document import Document
15from prompt_toolkit.filters import FilterOrBool, to_filter
16from prompt_toolkit.formatted_text import (
17 AnyFormattedText,
18 StyleAndTextTuples,
19 to_formatted_text,
20)
21from prompt_toolkit.formatted_text.utils import (
22 fragment_list_to_text,
23 fragment_list_width,
24 split_lines,
25)
26from prompt_toolkit.lexers import Lexer, SimpleLexer
27from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
28from prompt_toolkit.search import SearchState
29from prompt_toolkit.selection import SelectionType
30from prompt_toolkit.utils import get_cwidth
32from .processors import (
33 DisplayMultipleCursors,
34 HighlightIncrementalSearchProcessor,
35 HighlightSearchProcessor,
36 HighlightSelectionProcessor,
37 Processor,
38 TransformationInput,
39 merge_processors,
40)
42if TYPE_CHECKING:
43 from prompt_toolkit.key_binding.key_bindings import (
44 KeyBindingsBase,
45 NotImplementedOrNone,
46 )
47 from prompt_toolkit.utils import Event
50__all__ = [
51 "BufferControl",
52 "SearchBufferControl",
53 "DummyControl",
54 "FormattedTextControl",
55 "UIControl",
56 "UIContent",
57]
59GetLinePrefixCallable = Callable[[int, int], AnyFormattedText]
62class UIControl(metaclass=ABCMeta):
63 """
64 Base class for all user interface controls.
65 """
67 def reset(self) -> None:
68 # Default reset. (Doesn't have to be implemented.)
69 pass
71 def preferred_width(self, max_available_width: int) -> int | None:
72 return None
74 def preferred_height(
75 self,
76 width: int,
77 max_available_height: int,
78 wrap_lines: bool,
79 get_line_prefix: GetLinePrefixCallable | None,
80 ) -> int | None:
81 return None
83 def is_focusable(self) -> bool:
84 """
85 Tell whether this user control is focusable.
86 """
87 return False
89 @abstractmethod
90 def create_content(self, width: int, height: int) -> UIContent:
91 """
92 Generate the content for this user control.
94 Returns a :class:`.UIContent` instance.
95 """
97 def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
98 """
99 Handle mouse events.
101 When `NotImplemented` is returned, it means that the given event is not
102 handled by the `UIControl` itself. The `Window` or key bindings can
103 decide to handle this event as scrolling or changing focus.
105 :param mouse_event: `MouseEvent` instance.
106 """
107 return NotImplemented
109 def move_cursor_down(self) -> None:
110 """
111 Request to move the cursor down.
112 This happens when scrolling down and the cursor is completely at the
113 top.
114 """
116 def move_cursor_up(self) -> None:
117 """
118 Request to move the cursor up.
119 """
121 def get_key_bindings(self) -> KeyBindingsBase | None:
122 """
123 The key bindings that are specific for this user control.
125 Return a :class:`.KeyBindings` object if some key bindings are
126 specified, or `None` otherwise.
127 """
129 def get_invalidate_events(self) -> Iterable[Event[object]]:
130 """
131 Return a list of `Event` objects. This can be a generator.
132 (The application collects all these events, in order to bind redraw
133 handlers to these events.)
134 """
135 return []
138class UIContent:
139 """
140 Content generated by a user control. This content consists of a list of
141 lines.
143 :param get_line: Callable that takes a line number and returns the current
144 line. This is a list of (style_str, text) tuples.
145 :param line_count: The number of lines.
146 :param cursor_position: a :class:`.Point` for the cursor position.
147 :param menu_position: a :class:`.Point` for the menu position.
148 :param show_cursor: Make the cursor visible.
149 """
151 def __init__(
152 self,
153 get_line: Callable[[int], StyleAndTextTuples] = (lambda i: []),
154 line_count: int = 0,
155 cursor_position: Point | None = None,
156 menu_position: Point | None = None,
157 show_cursor: bool = True,
158 ):
159 self.get_line = get_line
160 self.line_count = line_count
161 self.cursor_position = cursor_position or Point(x=0, y=0)
162 self.menu_position = menu_position
163 self.show_cursor = show_cursor
165 # Cache for line heights. Maps cache key -> height
166 self._line_heights_cache: dict[Hashable, int] = {}
168 def __getitem__(self, lineno: int) -> StyleAndTextTuples:
169 "Make it iterable (iterate line by line)."
170 if lineno < self.line_count:
171 return self.get_line(lineno)
172 else:
173 raise IndexError
175 def get_height_for_line(
176 self,
177 lineno: int,
178 width: int,
179 get_line_prefix: GetLinePrefixCallable | None,
180 slice_stop: int | None = None,
181 ) -> int:
182 """
183 Return the height that a given line would need if it is rendered in a
184 space with the given width (using line wrapping).
186 :param get_line_prefix: None or a `Window.get_line_prefix` callable
187 that returns the prefix to be inserted before this line.
188 :param slice_stop: Wrap only "line[:slice_stop]" and return that
189 partial result. This is needed for scrolling the window correctly
190 when line wrapping.
191 :returns: The computed height.
192 """
193 # Instead of using `get_line_prefix` as key, we use render_counter
194 # instead. This is more reliable, because this function could still be
195 # the same, while the content would change over time.
196 key = get_app().render_counter, lineno, width, slice_stop
198 try:
199 return self._line_heights_cache[key]
200 except KeyError:
201 if width == 0:
202 height = 10**8
203 else:
204 # Calculate line width first.
205 line = fragment_list_to_text(self.get_line(lineno))[:slice_stop]
206 text_width = get_cwidth(line)
208 if get_line_prefix:
209 # Add prefix width.
210 text_width += fragment_list_width(
211 to_formatted_text(get_line_prefix(lineno, 0))
212 )
214 # Slower path: compute path when there's a line prefix.
215 height = 1
217 # Keep wrapping as long as the line doesn't fit.
218 # Keep adding new prefixes for every wrapped line.
219 while text_width > width:
220 height += 1
221 text_width -= width
223 fragments2 = to_formatted_text(
224 get_line_prefix(lineno, height - 1)
225 )
226 prefix_width = get_cwidth(fragment_list_to_text(fragments2))
228 if prefix_width >= width: # Prefix doesn't fit.
229 height = 10**8
230 break
232 text_width += prefix_width
233 else:
234 # Fast path: compute height when there's no line prefix.
235 try:
236 quotient, remainder = divmod(text_width, width)
237 except ZeroDivisionError:
238 height = 10**8
239 else:
240 if remainder:
241 quotient += 1 # Like math.ceil.
242 height = max(1, quotient)
244 # Cache and return
245 self._line_heights_cache[key] = height
246 return height
249class FormattedTextControl(UIControl):
250 """
251 Control that displays formatted text. This can be either plain text, an
252 :class:`~prompt_toolkit.formatted_text.HTML` object an
253 :class:`~prompt_toolkit.formatted_text.ANSI` object, a list of ``(style_str,
254 text)`` tuples or a callable that takes no argument and returns one of
255 those, depending on how you prefer to do the formatting. See
256 ``prompt_toolkit.layout.formatted_text`` for more information.
258 (It's mostly optimized for rather small widgets, like toolbars, menus, etc...)
260 When this UI control has the focus, the cursor will be shown in the upper
261 left corner of this control by default. There are two ways for specifying
262 the cursor position:
264 - Pass a `get_cursor_position` function which returns a `Point` instance
265 with the current cursor position.
267 - If the (formatted) text is passed as a list of ``(style, text)`` tuples
268 and there is one that looks like ``('[SetCursorPosition]', '')``, then
269 this will specify the cursor position.
271 Mouse support:
273 The list of fragments can also contain tuples of three items, looking like:
274 (style_str, text, handler). When mouse support is enabled and the user
275 clicks on this fragment, then the given handler is called. That handler
276 should accept two inputs: (Application, MouseEvent) and it should
277 either handle the event or return `NotImplemented` in case we want the
278 containing Window to handle this event.
280 :param focusable: `bool` or :class:`.Filter`: Tell whether this control is
281 focusable.
283 :param text: Text or formatted text to be displayed.
284 :param style: Style string applied to the content. (If you want to style
285 the whole :class:`~prompt_toolkit.layout.Window`, pass the style to the
286 :class:`~prompt_toolkit.layout.Window` instead.)
287 :param key_bindings: a :class:`.KeyBindings` object.
288 :param get_cursor_position: A callable that returns the cursor position as
289 a `Point` instance.
290 """
292 def __init__(
293 self,
294 text: AnyFormattedText = "",
295 style: str = "",
296 focusable: FilterOrBool = False,
297 key_bindings: KeyBindingsBase | None = None,
298 show_cursor: bool = True,
299 modal: bool = False,
300 get_cursor_position: Callable[[], Point | None] | None = None,
301 ) -> None:
302 self.text = text # No type check on 'text'. This is done dynamically.
303 self.style = style
304 self.focusable = to_filter(focusable)
306 # Key bindings.
307 self.key_bindings = key_bindings
308 self.show_cursor = show_cursor
309 self.modal = modal
310 self.get_cursor_position = get_cursor_position
312 #: Cache for the content.
313 self._content_cache: SimpleCache[Hashable, UIContent] = SimpleCache(maxsize=18)
314 self._fragment_cache: SimpleCache[int, StyleAndTextTuples] = SimpleCache(
315 maxsize=1
316 )
317 # Only cache one fragment list. We don't need the previous item.
319 # Render info for the mouse support.
320 self._fragments: StyleAndTextTuples | None = None
322 def reset(self) -> None:
323 self._fragments = None
325 def is_focusable(self) -> bool:
326 return self.focusable()
328 def __repr__(self) -> str:
329 return f"{self.__class__.__name__}({self.text!r})"
331 def _get_formatted_text_cached(self) -> StyleAndTextTuples:
332 """
333 Get fragments, but only retrieve fragments once during one render run.
334 (This function is called several times during one rendering, because
335 we also need those for calculating the dimensions.)
336 """
337 return self._fragment_cache.get(
338 get_app().render_counter, lambda: to_formatted_text(self.text, self.style)
339 )
341 def preferred_width(self, max_available_width: int) -> int:
342 """
343 Return the preferred width for this control.
344 That is the width of the longest line.
345 """
346 text = fragment_list_to_text(self._get_formatted_text_cached())
347 line_lengths = [get_cwidth(l) for l in text.split("\n")]
348 return max(line_lengths)
350 def preferred_height(
351 self,
352 width: int,
353 max_available_height: int,
354 wrap_lines: bool,
355 get_line_prefix: GetLinePrefixCallable | None,
356 ) -> int | None:
357 """
358 Return the preferred height for this control.
359 """
360 content = self.create_content(width, None)
361 if wrap_lines:
362 height = 0
363 for i in range(content.line_count):
364 height += content.get_height_for_line(i, width, get_line_prefix)
365 if height >= max_available_height:
366 return max_available_height
367 return height
368 else:
369 return content.line_count
371 def create_content(self, width: int, height: int | None) -> UIContent:
372 # Get fragments
373 fragments_with_mouse_handlers = self._get_formatted_text_cached()
374 fragment_lines_with_mouse_handlers = list(
375 split_lines(fragments_with_mouse_handlers)
376 )
378 # Strip mouse handlers from fragments.
379 fragment_lines: list[StyleAndTextTuples] = [
380 [(item[0], item[1]) for item in line]
381 for line in fragment_lines_with_mouse_handlers
382 ]
384 # Keep track of the fragments with mouse handler, for later use in
385 # `mouse_handler`.
386 self._fragments = fragments_with_mouse_handlers
388 # If there is a `[SetCursorPosition]` in the fragment list, set the
389 # cursor position here.
390 def get_cursor_position(
391 fragment: str = "[SetCursorPosition]",
392 ) -> Point | None:
393 for y, line in enumerate(fragment_lines):
394 x = 0
395 for style_str, text, *_ in line:
396 if fragment in style_str:
397 return Point(x=x, y=y)
398 x += len(text)
399 return None
401 # If there is a `[SetMenuPosition]`, set the menu over here.
402 def get_menu_position() -> Point | None:
403 return get_cursor_position("[SetMenuPosition]")
405 cursor_position = (self.get_cursor_position or get_cursor_position)()
407 # Create content, or take it from the cache.
408 key = (tuple(fragments_with_mouse_handlers), width, cursor_position)
410 def get_content() -> UIContent:
411 return UIContent(
412 get_line=lambda i: fragment_lines[i],
413 line_count=len(fragment_lines),
414 show_cursor=self.show_cursor,
415 cursor_position=cursor_position,
416 menu_position=get_menu_position(),
417 )
419 return self._content_cache.get(key, get_content)
421 def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
422 """
423 Handle mouse events.
425 (When the fragment list contained mouse handlers and the user clicked on
426 on any of these, the matching handler is called. This handler can still
427 return `NotImplemented` in case we want the
428 :class:`~prompt_toolkit.layout.Window` to handle this particular
429 event.)
430 """
431 if self._fragments:
432 # Read the generator.
433 fragments_for_line = list(split_lines(self._fragments))
435 try:
436 fragments = fragments_for_line[mouse_event.position.y]
437 except IndexError:
438 return NotImplemented
439 else:
440 # Find position in the fragment list.
441 xpos = mouse_event.position.x
443 # Find mouse handler for this character.
444 count = 0
445 for item in fragments:
446 count += len(item[1])
447 if count > xpos:
448 if len(item) >= 3:
449 # Handler found. Call it.
450 # (Handler can return NotImplemented, so return
451 # that result.)
452 handler = item[2] # type: ignore
453 return handler(mouse_event)
454 else:
455 break
457 # Otherwise, don't handle here.
458 return NotImplemented
460 def is_modal(self) -> bool:
461 return self.modal
463 def get_key_bindings(self) -> KeyBindingsBase | None:
464 return self.key_bindings
467class DummyControl(UIControl):
468 """
469 A dummy control object that doesn't paint any content.
471 Useful for filling a :class:`~prompt_toolkit.layout.Window`. (The
472 `fragment` and `char` attributes of the `Window` class can be used to
473 define the filling.)
474 """
476 def create_content(self, width: int, height: int) -> UIContent:
477 def get_line(i: int) -> StyleAndTextTuples:
478 return []
480 return UIContent(
481 get_line=get_line, line_count=100**100
482 ) # Something very big.
484 def is_focusable(self) -> bool:
485 return False
488class _ProcessedLine(NamedTuple):
489 fragments: StyleAndTextTuples
490 source_to_display: Callable[[int], int]
491 display_to_source: Callable[[int], int]
494class BufferControl(UIControl):
495 """
496 Control for visualising the content of a :class:`.Buffer`.
498 :param buffer: The :class:`.Buffer` object to be displayed.
499 :param input_processors: A list of
500 :class:`~prompt_toolkit.layout.processors.Processor` objects.
501 :param include_default_input_processors: When True, include the default
502 processors for highlighting of selection, search and displaying of
503 multiple cursors.
504 :param lexer: :class:`.Lexer` instance for syntax highlighting.
505 :param preview_search: `bool` or :class:`.Filter`: Show search while
506 typing. When this is `True`, probably you want to add a
507 ``HighlightIncrementalSearchProcessor`` as well. Otherwise only the
508 cursor position will move, but the text won't be highlighted.
509 :param focusable: `bool` or :class:`.Filter`: Tell whether this control is focusable.
510 :param focus_on_click: Focus this buffer when it's click, but not yet focused.
511 :param key_bindings: a :class:`.KeyBindings` object.
512 """
514 def __init__(
515 self,
516 buffer: Buffer | None = None,
517 input_processors: list[Processor] | None = None,
518 include_default_input_processors: bool = True,
519 lexer: Lexer | None = None,
520 preview_search: FilterOrBool = False,
521 focusable: FilterOrBool = True,
522 search_buffer_control: (
523 None | SearchBufferControl | Callable[[], SearchBufferControl]
524 ) = None,
525 menu_position: Callable[[], int | None] | None = None,
526 focus_on_click: FilterOrBool = False,
527 key_bindings: KeyBindingsBase | None = None,
528 ):
529 self.input_processors = input_processors
530 self.include_default_input_processors = include_default_input_processors
532 self.default_input_processors = [
533 HighlightSearchProcessor(),
534 HighlightIncrementalSearchProcessor(),
535 HighlightSelectionProcessor(),
536 DisplayMultipleCursors(),
537 ]
539 self.preview_search = to_filter(preview_search)
540 self.focusable = to_filter(focusable)
541 self.focus_on_click = to_filter(focus_on_click)
543 self.buffer = buffer or Buffer()
544 self.menu_position = menu_position
545 self.lexer = lexer or SimpleLexer()
546 self.key_bindings = key_bindings
547 self._search_buffer_control = search_buffer_control
549 #: Cache for the lexer.
550 #: Often, due to cursor movement, undo/redo and window resizing
551 #: operations, it happens that a short time, the same document has to be
552 #: lexed. This is a fairly easy way to cache such an expensive operation.
553 self._fragment_cache: SimpleCache[
554 Hashable, Callable[[int], StyleAndTextTuples]
555 ] = SimpleCache(maxsize=8)
557 self._last_click_timestamp: float | None = None
558 self._last_get_processed_line: Callable[[int], _ProcessedLine] | None = None
560 def __repr__(self) -> str:
561 return f"<{self.__class__.__name__} buffer={self.buffer!r} at {id(self)!r}>"
563 @property
564 def search_buffer_control(self) -> SearchBufferControl | None:
565 result: SearchBufferControl | None
567 if callable(self._search_buffer_control):
568 result = self._search_buffer_control()
569 else:
570 result = self._search_buffer_control
572 assert result is None or isinstance(result, SearchBufferControl)
573 return result
575 @property
576 def search_buffer(self) -> Buffer | None:
577 control = self.search_buffer_control
578 if control is not None:
579 return control.buffer
580 return None
582 @property
583 def search_state(self) -> SearchState:
584 """
585 Return the `SearchState` for searching this `BufferControl`. This is
586 always associated with the search control. If one search bar is used
587 for searching multiple `BufferControls`, then they share the same
588 `SearchState`.
589 """
590 search_buffer_control = self.search_buffer_control
591 if search_buffer_control:
592 return search_buffer_control.searcher_search_state
593 else:
594 return SearchState()
596 def is_focusable(self) -> bool:
597 return self.focusable()
599 def preferred_width(self, max_available_width: int) -> int | None:
600 """
601 This should return the preferred width.
603 Note: We don't specify a preferred width according to the content,
604 because it would be too expensive. Calculating the preferred
605 width can be done by calculating the longest line, but this would
606 require applying all the processors to each line. This is
607 unfeasible for a larger document, and doing it for small
608 documents only would result in inconsistent behaviour.
609 """
610 return None
612 def preferred_height(
613 self,
614 width: int,
615 max_available_height: int,
616 wrap_lines: bool,
617 get_line_prefix: GetLinePrefixCallable | None,
618 ) -> int | None:
619 # Calculate the content height, if it was drawn on a screen with the
620 # given width.
621 height = 0
622 content = self.create_content(width, height=1) # Pass a dummy '1' as height.
624 # When line wrapping is off, the height should be equal to the amount
625 # of lines.
626 if not wrap_lines:
627 return content.line_count
629 # When the number of lines exceeds the max_available_height, just
630 # return max_available_height. No need to calculate anything.
631 if content.line_count >= max_available_height:
632 return max_available_height
634 for i in range(content.line_count):
635 height += content.get_height_for_line(i, width, get_line_prefix)
637 if height >= max_available_height:
638 return max_available_height
640 return height
642 def _get_formatted_text_for_line_func(
643 self, document: Document
644 ) -> Callable[[int], StyleAndTextTuples]:
645 """
646 Create a function that returns the fragments for a given line.
647 """
649 # Cache using `document.text`.
650 def get_formatted_text_for_line() -> Callable[[int], StyleAndTextTuples]:
651 return self.lexer.lex_document(document)
653 key = (document.text, self.lexer.invalidation_hash())
654 return self._fragment_cache.get(key, get_formatted_text_for_line)
656 def _create_get_processed_line_func(
657 self, document: Document, width: int, height: int
658 ) -> Callable[[int], _ProcessedLine]:
659 """
660 Create a function that takes a line number of the current document and
661 returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source)
662 tuple.
663 """
664 # Merge all input processors together.
665 input_processors = self.input_processors or []
666 if self.include_default_input_processors:
667 input_processors = self.default_input_processors + input_processors
669 merged_processor = merge_processors(input_processors)
671 def transform(lineno: int, fragments: StyleAndTextTuples) -> _ProcessedLine:
672 "Transform the fragments for a given line number."
674 # Get cursor position at this line.
675 def source_to_display(i: int) -> int:
676 """X position from the buffer to the x position in the
677 processed fragment list. By default, we start from the 'identity'
678 operation."""
679 return i
681 transformation = merged_processor.apply_transformation(
682 TransformationInput(
683 self, document, lineno, source_to_display, fragments, width, height
684 )
685 )
687 return _ProcessedLine(
688 transformation.fragments,
689 transformation.source_to_display,
690 transformation.display_to_source,
691 )
693 def create_func() -> Callable[[int], _ProcessedLine]:
694 get_line = self._get_formatted_text_for_line_func(document)
695 cache: dict[int, _ProcessedLine] = {}
697 def get_processed_line(i: int) -> _ProcessedLine:
698 try:
699 return cache[i]
700 except KeyError:
701 processed_line = transform(i, get_line(i))
702 cache[i] = processed_line
703 return processed_line
705 return get_processed_line
707 return create_func()
709 def create_content(
710 self, width: int, height: int, preview_search: bool = False
711 ) -> UIContent:
712 """
713 Create a UIContent.
714 """
715 buffer = self.buffer
717 # Trigger history loading of the buffer. We do this during the
718 # rendering of the UI here, because it needs to happen when an
719 # `Application` with its event loop is running. During the rendering of
720 # the buffer control is the earliest place we can achieve this, where
721 # we're sure the right event loop is active, and don't require user
722 # interaction (like in a key binding).
723 buffer.load_history_if_not_yet_loaded()
725 # Get the document to be shown. If we are currently searching (the
726 # search buffer has focus, and the preview_search filter is enabled),
727 # then use the search document, which has possibly a different
728 # text/cursor position.)
729 search_control = self.search_buffer_control
730 preview_now = preview_search or bool(
731 # Only if this feature is enabled.
732 self.preview_search()
733 and
734 # And something was typed in the associated search field.
735 search_control
736 and search_control.buffer.text
737 and
738 # And we are searching in this control. (Many controls can point to
739 # the same search field, like in Pyvim.)
740 get_app().layout.search_target_buffer_control == self
741 )
743 if preview_now and search_control is not None:
744 ss = self.search_state
746 document = buffer.document_for_search(
747 SearchState(
748 text=search_control.buffer.text,
749 direction=ss.direction,
750 ignore_case=ss.ignore_case,
751 )
752 )
753 else:
754 document = buffer.document
756 get_processed_line = self._create_get_processed_line_func(
757 document, width, height
758 )
759 self._last_get_processed_line = get_processed_line
761 def translate_rowcol(row: int, col: int) -> Point:
762 "Return the content column for this coordinate."
763 return Point(x=get_processed_line(row).source_to_display(col), y=row)
765 def get_line(i: int) -> StyleAndTextTuples:
766 "Return the fragments for a given line number."
767 fragments = get_processed_line(i).fragments
769 # Add a space at the end, because that is a possible cursor
770 # position. (When inserting after the input.) We should do this on
771 # all the lines, not just the line containing the cursor. (Because
772 # otherwise, line wrapping/scrolling could change when moving the
773 # cursor around.)
774 fragments = fragments + [("", " ")]
775 return fragments
777 content = UIContent(
778 get_line=get_line,
779 line_count=document.line_count,
780 cursor_position=translate_rowcol(
781 document.cursor_position_row, document.cursor_position_col
782 ),
783 )
785 # If there is an auto completion going on, use that start point for a
786 # pop-up menu position. (But only when this buffer has the focus --
787 # there is only one place for a menu, determined by the focused buffer.)
788 if get_app().layout.current_control == self:
789 menu_position = self.menu_position() if self.menu_position else None
790 if menu_position is not None:
791 assert isinstance(menu_position, int)
792 menu_row, menu_col = buffer.document.translate_index_to_position(
793 menu_position
794 )
795 content.menu_position = translate_rowcol(menu_row, menu_col)
796 elif buffer.complete_state:
797 # Position for completion menu.
798 # Note: We use 'min', because the original cursor position could be
799 # behind the input string when the actual completion is for
800 # some reason shorter than the text we had before. (A completion
801 # can change and shorten the input.)
802 menu_row, menu_col = buffer.document.translate_index_to_position(
803 min(
804 buffer.cursor_position,
805 buffer.complete_state.original_document.cursor_position,
806 )
807 )
808 content.menu_position = translate_rowcol(menu_row, menu_col)
809 else:
810 content.menu_position = None
812 return content
814 def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
815 """
816 Mouse handler for this control.
817 """
818 buffer = self.buffer
819 position = mouse_event.position
821 # Focus buffer when clicked.
822 if get_app().layout.current_control == self:
823 if self._last_get_processed_line:
824 processed_line = self._last_get_processed_line(position.y)
826 # Translate coordinates back to the cursor position of the
827 # original input.
828 xpos = processed_line.display_to_source(position.x)
829 index = buffer.document.translate_row_col_to_index(position.y, xpos)
831 # Set the cursor position.
832 if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
833 buffer.exit_selection()
834 buffer.cursor_position = index
836 elif (
837 mouse_event.event_type == MouseEventType.MOUSE_MOVE
838 and mouse_event.button != MouseButton.NONE
839 ):
840 # Click and drag to highlight a selection
841 if (
842 buffer.selection_state is None
843 and abs(buffer.cursor_position - index) > 0
844 ):
845 buffer.start_selection(selection_type=SelectionType.CHARACTERS)
846 buffer.cursor_position = index
848 elif mouse_event.event_type == MouseEventType.MOUSE_UP:
849 # When the cursor was moved to another place, select the text.
850 # (The >1 is actually a small but acceptable workaround for
851 # selecting text in Vi navigation mode. In navigation mode,
852 # the cursor can never be after the text, so the cursor
853 # will be repositioned automatically.)
854 if abs(buffer.cursor_position - index) > 1:
855 if buffer.selection_state is None:
856 buffer.start_selection(
857 selection_type=SelectionType.CHARACTERS
858 )
859 buffer.cursor_position = index
861 # Select word around cursor on double click.
862 # Two MOUSE_UP events in a short timespan are considered a double click.
863 double_click = (
864 self._last_click_timestamp
865 and time.time() - self._last_click_timestamp < 0.3
866 )
867 self._last_click_timestamp = time.time()
869 if double_click:
870 start, end = buffer.document.find_boundaries_of_current_word()
871 buffer.cursor_position += start
872 buffer.start_selection(selection_type=SelectionType.CHARACTERS)
873 buffer.cursor_position += end - start
874 else:
875 # Don't handle scroll events here.
876 return NotImplemented
878 # Not focused, but focusing on click events.
879 else:
880 if (
881 self.focus_on_click()
882 and mouse_event.event_type == MouseEventType.MOUSE_UP
883 ):
884 # Focus happens on mouseup. (If we did this on mousedown, the
885 # up event will be received at the point where this widget is
886 # focused and be handled anyway.)
887 get_app().layout.current_control = self
888 else:
889 return NotImplemented
891 return None
893 def move_cursor_down(self) -> None:
894 b = self.buffer
895 b.cursor_position += b.document.get_cursor_down_position()
897 def move_cursor_up(self) -> None:
898 b = self.buffer
899 b.cursor_position += b.document.get_cursor_up_position()
901 def get_key_bindings(self) -> KeyBindingsBase | None:
902 """
903 When additional key bindings are given. Return these.
904 """
905 return self.key_bindings
907 def get_invalidate_events(self) -> Iterable[Event[object]]:
908 """
909 Return the Window invalidate events.
910 """
911 # Whenever the buffer changes, the UI has to be updated.
912 yield self.buffer.on_text_changed
913 yield self.buffer.on_cursor_position_changed
915 yield self.buffer.on_completions_changed
916 yield self.buffer.on_suggestion_set
919class SearchBufferControl(BufferControl):
920 """
921 :class:`.BufferControl` which is used for searching another
922 :class:`.BufferControl`.
924 :param ignore_case: Search case insensitive.
925 """
927 def __init__(
928 self,
929 buffer: Buffer | None = None,
930 input_processors: list[Processor] | None = None,
931 lexer: Lexer | None = None,
932 focus_on_click: FilterOrBool = False,
933 key_bindings: KeyBindingsBase | None = None,
934 ignore_case: FilterOrBool = False,
935 ):
936 super().__init__(
937 buffer=buffer,
938 input_processors=input_processors,
939 lexer=lexer,
940 focus_on_click=focus_on_click,
941 key_bindings=key_bindings,
942 )
944 # If this BufferControl is used as a search field for one or more other
945 # BufferControls, then represents the search state.
946 self.searcher_search_state = SearchState(ignore_case=ignore_case)