1"""
2User interface Controls for the layout.
3"""
4
5from __future__ import annotations
6
7import time
8from abc import ABCMeta, abstractmethod
9from typing import TYPE_CHECKING, Callable, Hashable, Iterable, NamedTuple
10
11from prompt_toolkit.application.current import get_app
12from prompt_toolkit.buffer import Buffer
13from prompt_toolkit.cache import SimpleCache
14from prompt_toolkit.data_structures import Point
15from prompt_toolkit.document import Document
16from prompt_toolkit.filters import FilterOrBool, to_filter
17from prompt_toolkit.formatted_text import (
18 AnyFormattedText,
19 StyleAndTextTuples,
20 to_formatted_text,
21)
22from prompt_toolkit.formatted_text.utils import (
23 fragment_list_to_text,
24 fragment_list_width,
25 split_lines,
26)
27from prompt_toolkit.lexers import Lexer, SimpleLexer
28from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
29from prompt_toolkit.search import SearchState
30from prompt_toolkit.selection import SelectionType
31from prompt_toolkit.utils import get_cwidth
32
33from .processors import (
34 DisplayMultipleCursors,
35 HighlightIncrementalSearchProcessor,
36 HighlightSearchProcessor,
37 HighlightSelectionProcessor,
38 Processor,
39 TransformationInput,
40 merge_processors,
41)
42
43if TYPE_CHECKING:
44 from prompt_toolkit.key_binding.key_bindings import (
45 KeyBindingsBase,
46 NotImplementedOrNone,
47 )
48 from prompt_toolkit.utils import Event
49
50
51__all__ = [
52 "BufferControl",
53 "SearchBufferControl",
54 "DummyControl",
55 "FormattedTextControl",
56 "UIControl",
57 "UIContent",
58]
59
60GetLinePrefixCallable = Callable[[int, int], AnyFormattedText]
61
62
63class UIControl(metaclass=ABCMeta):
64 """
65 Base class for all user interface controls.
66 """
67
68 def reset(self) -> None:
69 # Default reset. (Doesn't have to be implemented.)
70 pass
71
72 def preferred_width(self, max_available_width: int) -> int | None:
73 return None
74
75 def preferred_height(
76 self,
77 width: int,
78 max_available_height: int,
79 wrap_lines: bool,
80 get_line_prefix: GetLinePrefixCallable | None,
81 ) -> int | None:
82 return None
83
84 def is_focusable(self) -> bool:
85 """
86 Tell whether this user control is focusable.
87 """
88 return False
89
90 @abstractmethod
91 def create_content(self, width: int, height: int) -> UIContent:
92 """
93 Generate the content for this user control.
94
95 Returns a :class:`.UIContent` instance.
96 """
97
98 def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
99 """
100 Handle mouse events.
101
102 When `NotImplemented` is returned, it means that the given event is not
103 handled by the `UIControl` itself. The `Window` or key bindings can
104 decide to handle this event as scrolling or changing focus.
105
106 :param mouse_event: `MouseEvent` instance.
107 """
108 return NotImplemented
109
110 def move_cursor_down(self) -> None:
111 """
112 Request to move the cursor down.
113 This happens when scrolling down and the cursor is completely at the
114 top.
115 """
116
117 def move_cursor_up(self) -> None:
118 """
119 Request to move the cursor up.
120 """
121
122 def get_key_bindings(self) -> KeyBindingsBase | None:
123 """
124 The key bindings that are specific for this user control.
125
126 Return a :class:`.KeyBindings` object if some key bindings are
127 specified, or `None` otherwise.
128 """
129
130 def get_invalidate_events(self) -> Iterable[Event[object]]:
131 """
132 Return a list of `Event` objects. This can be a generator.
133 (The application collects all these events, in order to bind redraw
134 handlers to these events.)
135 """
136 return []
137
138
139class UIContent:
140 """
141 Content generated by a user control. This content consists of a list of
142 lines.
143
144 :param get_line: Callable that takes a line number and returns the current
145 line. This is a list of (style_str, text) tuples.
146 :param line_count: The number of lines.
147 :param cursor_position: a :class:`.Point` for the cursor position.
148 :param menu_position: a :class:`.Point` for the menu position.
149 :param show_cursor: Make the cursor visible.
150 """
151
152 def __init__(
153 self,
154 get_line: Callable[[int], StyleAndTextTuples] = (lambda i: []),
155 line_count: int = 0,
156 cursor_position: Point | None = None,
157 menu_position: Point | None = None,
158 show_cursor: bool = True,
159 ):
160 self.get_line = get_line
161 self.line_count = line_count
162 self.cursor_position = cursor_position or Point(x=0, y=0)
163 self.menu_position = menu_position
164 self.show_cursor = show_cursor
165
166 # Cache for line heights. Maps cache key -> height
167 self._line_heights_cache: dict[Hashable, int] = {}
168
169 def __getitem__(self, lineno: int) -> StyleAndTextTuples:
170 "Make it iterable (iterate line by line)."
171 if lineno < self.line_count:
172 return self.get_line(lineno)
173 else:
174 raise IndexError
175
176 def get_height_for_line(
177 self,
178 lineno: int,
179 width: int,
180 get_line_prefix: GetLinePrefixCallable | None,
181 slice_stop: int | None = None,
182 ) -> int:
183 """
184 Return the height that a given line would need if it is rendered in a
185 space with the given width (using line wrapping).
186
187 :param get_line_prefix: None or a `Window.get_line_prefix` callable
188 that returns the prefix to be inserted before this line.
189 :param slice_stop: Wrap only "line[:slice_stop]" and return that
190 partial result. This is needed for scrolling the window correctly
191 when line wrapping.
192 :returns: The computed height.
193 """
194 # Instead of using `get_line_prefix` as key, we use render_counter
195 # instead. This is more reliable, because this function could still be
196 # the same, while the content would change over time.
197 key = get_app().render_counter, lineno, width, slice_stop
198
199 try:
200 return self._line_heights_cache[key]
201 except KeyError:
202 if width == 0:
203 height = 10**8
204 else:
205 # Calculate line width first.
206 line = fragment_list_to_text(self.get_line(lineno))[:slice_stop]
207 text_width = get_cwidth(line)
208
209 if get_line_prefix:
210 # Add prefix width.
211 text_width += fragment_list_width(
212 to_formatted_text(get_line_prefix(lineno, 0))
213 )
214
215 # Slower path: compute path when there's a line prefix.
216 height = 1
217
218 # Keep wrapping as long as the line doesn't fit.
219 # Keep adding new prefixes for every wrapped line.
220 while text_width > width:
221 height += 1
222 text_width -= width
223
224 fragments2 = to_formatted_text(
225 get_line_prefix(lineno, height - 1)
226 )
227 prefix_width = get_cwidth(fragment_list_to_text(fragments2))
228
229 if prefix_width >= width: # Prefix doesn't fit.
230 height = 10**8
231 break
232
233 text_width += prefix_width
234 else:
235 # Fast path: compute height when there's no line prefix.
236 try:
237 quotient, remainder = divmod(text_width, width)
238 except ZeroDivisionError:
239 height = 10**8
240 else:
241 if remainder:
242 quotient += 1 # Like math.ceil.
243 height = max(1, quotient)
244
245 # Cache and return
246 self._line_heights_cache[key] = height
247 return height
248
249
250class FormattedTextControl(UIControl):
251 """
252 Control that displays formatted text. This can be either plain text, an
253 :class:`~prompt_toolkit.formatted_text.HTML` object an
254 :class:`~prompt_toolkit.formatted_text.ANSI` object, a list of ``(style_str,
255 text)`` tuples or a callable that takes no argument and returns one of
256 those, depending on how you prefer to do the formatting. See
257 ``prompt_toolkit.layout.formatted_text`` for more information.
258
259 (It's mostly optimized for rather small widgets, like toolbars, menus, etc...)
260
261 When this UI control has the focus, the cursor will be shown in the upper
262 left corner of this control by default. There are two ways for specifying
263 the cursor position:
264
265 - Pass a `get_cursor_position` function which returns a `Point` instance
266 with the current cursor position.
267
268 - If the (formatted) text is passed as a list of ``(style, text)`` tuples
269 and there is one that looks like ``('[SetCursorPosition]', '')``, then
270 this will specify the cursor position.
271
272 Mouse support:
273
274 The list of fragments can also contain tuples of three items, looking like:
275 (style_str, text, handler). When mouse support is enabled and the user
276 clicks on this fragment, then the given handler is called. That handler
277 should accept two inputs: (Application, MouseEvent) and it should
278 either handle the event or return `NotImplemented` in case we want the
279 containing Window to handle this event.
280
281 :param focusable: `bool` or :class:`.Filter`: Tell whether this control is
282 focusable.
283
284 :param text: Text or formatted text to be displayed.
285 :param style: Style string applied to the content. (If you want to style
286 the whole :class:`~prompt_toolkit.layout.Window`, pass the style to the
287 :class:`~prompt_toolkit.layout.Window` instead.)
288 :param key_bindings: a :class:`.KeyBindings` object.
289 :param get_cursor_position: A callable that returns the cursor position as
290 a `Point` instance.
291 """
292
293 def __init__(
294 self,
295 text: AnyFormattedText = "",
296 style: str = "",
297 focusable: FilterOrBool = False,
298 key_bindings: KeyBindingsBase | None = None,
299 show_cursor: bool = True,
300 modal: bool = False,
301 get_cursor_position: Callable[[], Point | None] | None = None,
302 ) -> None:
303 self.text = text # No type check on 'text'. This is done dynamically.
304 self.style = style
305 self.focusable = to_filter(focusable)
306
307 # Key bindings.
308 self.key_bindings = key_bindings
309 self.show_cursor = show_cursor
310 self.modal = modal
311 self.get_cursor_position = get_cursor_position
312
313 #: Cache for the content.
314 self._content_cache: SimpleCache[Hashable, UIContent] = SimpleCache(maxsize=18)
315 self._fragment_cache: SimpleCache[int, StyleAndTextTuples] = SimpleCache(
316 maxsize=1
317 )
318 # Only cache one fragment list. We don't need the previous item.
319
320 # Render info for the mouse support.
321 self._fragments: StyleAndTextTuples | None = None
322
323 def reset(self) -> None:
324 self._fragments = None
325
326 def is_focusable(self) -> bool:
327 return self.focusable()
328
329 def __repr__(self) -> str:
330 return f"{self.__class__.__name__}({self.text!r})"
331
332 def _get_formatted_text_cached(self) -> StyleAndTextTuples:
333 """
334 Get fragments, but only retrieve fragments once during one render run.
335 (This function is called several times during one rendering, because
336 we also need those for calculating the dimensions.)
337 """
338 return self._fragment_cache.get(
339 get_app().render_counter, lambda: to_formatted_text(self.text, self.style)
340 )
341
342 def preferred_width(self, max_available_width: int) -> int:
343 """
344 Return the preferred width for this control.
345 That is the width of the longest line.
346 """
347 text = fragment_list_to_text(self._get_formatted_text_cached())
348 line_lengths = [get_cwidth(l) for l in text.split("\n")]
349 return max(line_lengths)
350
351 def preferred_height(
352 self,
353 width: int,
354 max_available_height: int,
355 wrap_lines: bool,
356 get_line_prefix: GetLinePrefixCallable | None,
357 ) -> int | None:
358 """
359 Return the preferred height for this control.
360 """
361 content = self.create_content(width, None)
362 if wrap_lines:
363 height = 0
364 for i in range(content.line_count):
365 height += content.get_height_for_line(i, width, get_line_prefix)
366 if height >= max_available_height:
367 return max_available_height
368 return height
369 else:
370 return content.line_count
371
372 def create_content(self, width: int, height: int | None) -> UIContent:
373 # Get fragments
374 fragments_with_mouse_handlers = self._get_formatted_text_cached()
375 fragment_lines_with_mouse_handlers = list(
376 split_lines(fragments_with_mouse_handlers)
377 )
378
379 # Strip mouse handlers from fragments.
380 fragment_lines: list[StyleAndTextTuples] = [
381 [(item[0], item[1]) for item in line]
382 for line in fragment_lines_with_mouse_handlers
383 ]
384
385 # Keep track of the fragments with mouse handler, for later use in
386 # `mouse_handler`.
387 self._fragments = fragments_with_mouse_handlers
388
389 # If there is a `[SetCursorPosition]` in the fragment list, set the
390 # cursor position here.
391 def get_cursor_position(
392 fragment: str = "[SetCursorPosition]",
393 ) -> Point | None:
394 for y, line in enumerate(fragment_lines):
395 x = 0
396 for style_str, text, *_ in line:
397 if fragment in style_str:
398 return Point(x=x, y=y)
399 x += len(text)
400 return None
401
402 # If there is a `[SetMenuPosition]`, set the menu over here.
403 def get_menu_position() -> Point | None:
404 return get_cursor_position("[SetMenuPosition]")
405
406 cursor_position = (self.get_cursor_position or get_cursor_position)()
407
408 # Create content, or take it from the cache.
409 key = (tuple(fragments_with_mouse_handlers), width, cursor_position)
410
411 def get_content() -> UIContent:
412 return UIContent(
413 get_line=lambda i: fragment_lines[i],
414 line_count=len(fragment_lines),
415 show_cursor=self.show_cursor,
416 cursor_position=cursor_position,
417 menu_position=get_menu_position(),
418 )
419
420 return self._content_cache.get(key, get_content)
421
422 def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
423 """
424 Handle mouse events.
425
426 (When the fragment list contained mouse handlers and the user clicked on
427 on any of these, the matching handler is called. This handler can still
428 return `NotImplemented` in case we want the
429 :class:`~prompt_toolkit.layout.Window` to handle this particular
430 event.)
431 """
432 if self._fragments:
433 # Read the generator.
434 fragments_for_line = list(split_lines(self._fragments))
435
436 try:
437 fragments = fragments_for_line[mouse_event.position.y]
438 except IndexError:
439 return NotImplemented
440 else:
441 # Find position in the fragment list.
442 xpos = mouse_event.position.x
443
444 # Find mouse handler for this character.
445 count = 0
446 for item in fragments:
447 count += len(item[1])
448 if count > xpos:
449 if len(item) >= 3:
450 # Handler found. Call it.
451 # (Handler can return NotImplemented, so return
452 # that result.)
453 handler = item[2]
454 return handler(mouse_event)
455 else:
456 break
457
458 # Otherwise, don't handle here.
459 return NotImplemented
460
461 def is_modal(self) -> bool:
462 return self.modal
463
464 def get_key_bindings(self) -> KeyBindingsBase | None:
465 return self.key_bindings
466
467
468class DummyControl(UIControl):
469 """
470 A dummy control object that doesn't paint any content.
471
472 Useful for filling a :class:`~prompt_toolkit.layout.Window`. (The
473 `fragment` and `char` attributes of the `Window` class can be used to
474 define the filling.)
475 """
476
477 def create_content(self, width: int, height: int) -> UIContent:
478 def get_line(i: int) -> StyleAndTextTuples:
479 return []
480
481 return UIContent(get_line=get_line, line_count=100**100) # Something very big.
482
483 def is_focusable(self) -> bool:
484 return False
485
486
487class _ProcessedLine(NamedTuple):
488 fragments: StyleAndTextTuples
489 source_to_display: Callable[[int], int]
490 display_to_source: Callable[[int], int]
491
492
493class BufferControl(UIControl):
494 """
495 Control for visualizing the content of a :class:`.Buffer`.
496
497 :param buffer: The :class:`.Buffer` object to be displayed.
498 :param input_processors: A list of
499 :class:`~prompt_toolkit.layout.processors.Processor` objects.
500 :param include_default_input_processors: When True, include the default
501 processors for highlighting of selection, search and displaying of
502 multiple cursors.
503 :param lexer: :class:`.Lexer` instance for syntax highlighting.
504 :param preview_search: `bool` or :class:`.Filter`: Show search while
505 typing. When this is `True`, probably you want to add a
506 ``HighlightIncrementalSearchProcessor`` as well. Otherwise only the
507 cursor position will move, but the text won't be highlighted.
508 :param focusable: `bool` or :class:`.Filter`: Tell whether this control is focusable.
509 :param focus_on_click: Focus this buffer when it's click, but not yet focused.
510 :param key_bindings: a :class:`.KeyBindings` object.
511 """
512
513 def __init__(
514 self,
515 buffer: Buffer | None = None,
516 input_processors: list[Processor] | None = None,
517 include_default_input_processors: bool = True,
518 lexer: Lexer | None = None,
519 preview_search: FilterOrBool = False,
520 focusable: FilterOrBool = True,
521 search_buffer_control: (
522 None | SearchBufferControl | Callable[[], SearchBufferControl]
523 ) = None,
524 menu_position: Callable[[], int | None] | None = None,
525 focus_on_click: FilterOrBool = False,
526 key_bindings: KeyBindingsBase | None = None,
527 ):
528 self.input_processors = input_processors
529 self.include_default_input_processors = include_default_input_processors
530
531 self.default_input_processors = [
532 HighlightSearchProcessor(),
533 HighlightIncrementalSearchProcessor(),
534 HighlightSelectionProcessor(),
535 DisplayMultipleCursors(),
536 ]
537
538 self.preview_search = to_filter(preview_search)
539 self.focusable = to_filter(focusable)
540 self.focus_on_click = to_filter(focus_on_click)
541
542 self.buffer = buffer or Buffer()
543 self.menu_position = menu_position
544 self.lexer = lexer or SimpleLexer()
545 self.key_bindings = key_bindings
546 self._search_buffer_control = search_buffer_control
547
548 #: Cache for the lexer.
549 #: Often, due to cursor movement, undo/redo and window resizing
550 #: operations, it happens that a short time, the same document has to be
551 #: lexed. This is a fairly easy way to cache such an expensive operation.
552 self._fragment_cache: SimpleCache[
553 Hashable, Callable[[int], StyleAndTextTuples]
554 ] = SimpleCache(maxsize=8)
555
556 self._last_click_timestamp: float | None = None
557 self._last_get_processed_line: Callable[[int], _ProcessedLine] | None = None
558
559 def __repr__(self) -> str:
560 return f"<{self.__class__.__name__} buffer={self.buffer!r} at {id(self)!r}>"
561
562 @property
563 def search_buffer_control(self) -> SearchBufferControl | None:
564 result: SearchBufferControl | None
565
566 if callable(self._search_buffer_control):
567 result = self._search_buffer_control()
568 else:
569 result = self._search_buffer_control
570
571 assert result is None or isinstance(result, SearchBufferControl)
572 return result
573
574 @property
575 def search_buffer(self) -> Buffer | None:
576 control = self.search_buffer_control
577 if control is not None:
578 return control.buffer
579 return None
580
581 @property
582 def search_state(self) -> SearchState:
583 """
584 Return the `SearchState` for searching this `BufferControl`. This is
585 always associated with the search control. If one search bar is used
586 for searching multiple `BufferControls`, then they share the same
587 `SearchState`.
588 """
589 search_buffer_control = self.search_buffer_control
590 if search_buffer_control:
591 return search_buffer_control.searcher_search_state
592 else:
593 return SearchState()
594
595 def is_focusable(self) -> bool:
596 return self.focusable()
597
598 def preferred_width(self, max_available_width: int) -> int | None:
599 """
600 This should return the preferred width.
601
602 Note: We don't specify a preferred width according to the content,
603 because it would be too expensive. Calculating the preferred
604 width can be done by calculating the longest line, but this would
605 require applying all the processors to each line. This is
606 unfeasible for a larger document, and doing it for small
607 documents only would result in inconsistent behavior.
608 """
609 return None
610
611 def preferred_height(
612 self,
613 width: int,
614 max_available_height: int,
615 wrap_lines: bool,
616 get_line_prefix: GetLinePrefixCallable | None,
617 ) -> int | None:
618 # Calculate the content height, if it was drawn on a screen with the
619 # given width.
620 height = 0
621 content = self.create_content(width, height=1) # Pass a dummy '1' as height.
622
623 # When line wrapping is off, the height should be equal to the amount
624 # of lines.
625 if not wrap_lines:
626 return content.line_count
627
628 # When the number of lines exceeds the max_available_height, just
629 # return max_available_height. No need to calculate anything.
630 if content.line_count >= max_available_height:
631 return max_available_height
632
633 for i in range(content.line_count):
634 height += content.get_height_for_line(i, width, get_line_prefix)
635
636 if height >= max_available_height:
637 return max_available_height
638
639 return height
640
641 def _get_formatted_text_for_line_func(
642 self, document: Document
643 ) -> Callable[[int], StyleAndTextTuples]:
644 """
645 Create a function that returns the fragments for a given line.
646 """
647
648 # Cache using `document.text`.
649 def get_formatted_text_for_line() -> Callable[[int], StyleAndTextTuples]:
650 return self.lexer.lex_document(document)
651
652 key = (document.text, self.lexer.invalidation_hash())
653 return self._fragment_cache.get(key, get_formatted_text_for_line)
654
655 def _create_get_processed_line_func(
656 self, document: Document, width: int, height: int
657 ) -> Callable[[int], _ProcessedLine]:
658 """
659 Create a function that takes a line number of the current document and
660 returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source)
661 tuple.
662 """
663 # Merge all input processors together.
664 input_processors = self.input_processors or []
665 if self.include_default_input_processors:
666 input_processors = self.default_input_processors + input_processors
667
668 merged_processor = merge_processors(input_processors)
669
670 def transform(
671 lineno: int,
672 fragments: StyleAndTextTuples,
673 get_line: Callable[[int], StyleAndTextTuples],
674 ) -> _ProcessedLine:
675 "Transform the fragments for a given line number."
676
677 # Get cursor position at this line.
678 def source_to_display(i: int) -> int:
679 """X position from the buffer to the x position in the
680 processed fragment list. By default, we start from the 'identity'
681 operation."""
682 return i
683
684 transformation = merged_processor.apply_transformation(
685 TransformationInput(
686 self,
687 document,
688 lineno,
689 source_to_display,
690 fragments,
691 width,
692 height,
693 get_line,
694 )
695 )
696
697 return _ProcessedLine(
698 transformation.fragments,
699 transformation.source_to_display,
700 transformation.display_to_source,
701 )
702
703 def create_func() -> Callable[[int], _ProcessedLine]:
704 get_line = self._get_formatted_text_for_line_func(document)
705 cache: dict[int, _ProcessedLine] = {}
706
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), get_line)
712 cache[i] = processed_line
713 return processed_line
714
715 return get_processed_line
716
717 return create_func()
718
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
726
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()
734
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 )
752
753 if preview_now and search_control is not None:
754 ss = self.search_state
755
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
765
766 get_processed_line = self._create_get_processed_line_func(
767 document, width, height
768 )
769 self._last_get_processed_line = get_processed_line
770
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)
774
775 def get_line(i: int) -> StyleAndTextTuples:
776 "Return the fragments for a given line number."
777 fragments = get_processed_line(i).fragments
778
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
786
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 )
794
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
821
822 return content
823
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
830
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)
835
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)
840
841 # Set the cursor position.
842 if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
843 buffer.exit_selection()
844 buffer.cursor_position = index
845
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
857
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
870
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()
878
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
887
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
900
901 return None
902
903 def move_cursor_down(self) -> None:
904 b = self.buffer
905 b.cursor_position += b.document.get_cursor_down_position()
906
907 def move_cursor_up(self) -> None:
908 b = self.buffer
909 b.cursor_position += b.document.get_cursor_up_position()
910
911 def get_key_bindings(self) -> KeyBindingsBase | None:
912 """
913 When additional key bindings are given. Return these.
914 """
915 return self.key_bindings
916
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
924
925 yield self.buffer.on_completions_changed
926 yield self.buffer.on_suggestion_set
927
928
929class SearchBufferControl(BufferControl):
930 """
931 :class:`.BufferControl` which is used for searching another
932 :class:`.BufferControl`.
933
934 :param ignore_case: Search case insensitive.
935 """
936
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 )
953
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)