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