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