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.4.4, created at 2024-04-20 06:09 +0000

1""" 

2User interface Controls for the layout. 

3""" 

4from __future__ import annotations 

5 

6import time 

7from abc import ABCMeta, abstractmethod 

8from typing import TYPE_CHECKING, Callable, Hashable, Iterable, NamedTuple 

9 

10from prompt_toolkit.application.current import get_app 

11from prompt_toolkit.buffer import Buffer 

12from prompt_toolkit.cache import SimpleCache 

13from prompt_toolkit.data_structures import Point 

14from prompt_toolkit.document import Document 

15from prompt_toolkit.filters import FilterOrBool, to_filter 

16from prompt_toolkit.formatted_text import ( 

17 AnyFormattedText, 

18 StyleAndTextTuples, 

19 to_formatted_text, 

20) 

21from prompt_toolkit.formatted_text.utils import ( 

22 fragment_list_to_text, 

23 fragment_list_width, 

24 split_lines, 

25) 

26from prompt_toolkit.lexers import Lexer, SimpleLexer 

27from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType 

28from prompt_toolkit.search import SearchState 

29from prompt_toolkit.selection import SelectionType 

30from prompt_toolkit.utils import get_cwidth 

31 

32from .processors import ( 

33 DisplayMultipleCursors, 

34 HighlightIncrementalSearchProcessor, 

35 HighlightSearchProcessor, 

36 HighlightSelectionProcessor, 

37 Processor, 

38 TransformationInput, 

39 merge_processors, 

40) 

41 

42if TYPE_CHECKING: 

43 from prompt_toolkit.key_binding.key_bindings import ( 

44 KeyBindingsBase, 

45 NotImplementedOrNone, 

46 ) 

47 from prompt_toolkit.utils import Event 

48 

49 

50__all__ = [ 

51 "BufferControl", 

52 "SearchBufferControl", 

53 "DummyControl", 

54 "FormattedTextControl", 

55 "UIControl", 

56 "UIContent", 

57] 

58 

59GetLinePrefixCallable = Callable[[int, int], AnyFormattedText] 

60 

61 

62class UIControl(metaclass=ABCMeta): 

63 """ 

64 Base class for all user interface controls. 

65 """ 

66 

67 def reset(self) -> None: 

68 # Default reset. (Doesn't have to be implemented.) 

69 pass 

70 

71 def preferred_width(self, max_available_width: int) -> int | None: 

72 return None 

73 

74 def preferred_height( 

75 self, 

76 width: int, 

77 max_available_height: int, 

78 wrap_lines: bool, 

79 get_line_prefix: GetLinePrefixCallable | None, 

80 ) -> int | None: 

81 return None 

82 

83 def is_focusable(self) -> bool: 

84 """ 

85 Tell whether this user control is focusable. 

86 """ 

87 return False 

88 

89 @abstractmethod 

90 def create_content(self, width: int, height: int) -> UIContent: 

91 """ 

92 Generate the content for this user control. 

93 

94 Returns a :class:`.UIContent` instance. 

95 """ 

96 

97 def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: 

98 """ 

99 Handle mouse events. 

100 

101 When `NotImplemented` is returned, it means that the given event is not 

102 handled by the `UIControl` itself. The `Window` or key bindings can 

103 decide to handle this event as scrolling or changing focus. 

104 

105 :param mouse_event: `MouseEvent` instance. 

106 """ 

107 return NotImplemented 

108 

109 def move_cursor_down(self) -> None: 

110 """ 

111 Request to move the cursor down. 

112 This happens when scrolling down and the cursor is completely at the 

113 top. 

114 """ 

115 

116 def move_cursor_up(self) -> None: 

117 """ 

118 Request to move the cursor up. 

119 """ 

120 

121 def get_key_bindings(self) -> KeyBindingsBase | None: 

122 """ 

123 The key bindings that are specific for this user control. 

124 

125 Return a :class:`.KeyBindings` object if some key bindings are 

126 specified, or `None` otherwise. 

127 """ 

128 

129 def get_invalidate_events(self) -> Iterable[Event[object]]: 

130 """ 

131 Return a list of `Event` objects. This can be a generator. 

132 (The application collects all these events, in order to bind redraw 

133 handlers to these events.) 

134 """ 

135 return [] 

136 

137 

138class UIContent: 

139 """ 

140 Content generated by a user control. This content consists of a list of 

141 lines. 

142 

143 :param get_line: Callable that takes a line number and returns the current 

144 line. This is a list of (style_str, text) tuples. 

145 :param line_count: The number of lines. 

146 :param cursor_position: a :class:`.Point` for the cursor position. 

147 :param menu_position: a :class:`.Point` for the menu position. 

148 :param show_cursor: Make the cursor visible. 

149 """ 

150 

151 def __init__( 

152 self, 

153 get_line: Callable[[int], StyleAndTextTuples] = (lambda i: []), 

154 line_count: int = 0, 

155 cursor_position: Point | None = None, 

156 menu_position: Point | None = None, 

157 show_cursor: bool = True, 

158 ): 

159 self.get_line = get_line 

160 self.line_count = line_count 

161 self.cursor_position = cursor_position or Point(x=0, y=0) 

162 self.menu_position = menu_position 

163 self.show_cursor = show_cursor 

164 

165 # Cache for line heights. Maps cache key -> height 

166 self._line_heights_cache: dict[Hashable, int] = {} 

167 

168 def __getitem__(self, lineno: int) -> StyleAndTextTuples: 

169 "Make it iterable (iterate line by line)." 

170 if lineno < self.line_count: 

171 return self.get_line(lineno) 

172 else: 

173 raise IndexError 

174 

175 def get_height_for_line( 

176 self, 

177 lineno: int, 

178 width: int, 

179 get_line_prefix: GetLinePrefixCallable | None, 

180 slice_stop: int | None = None, 

181 ) -> int: 

182 """ 

183 Return the height that a given line would need if it is rendered in a 

184 space with the given width (using line wrapping). 

185 

186 :param get_line_prefix: None or a `Window.get_line_prefix` callable 

187 that returns the prefix to be inserted before this line. 

188 :param slice_stop: Wrap only "line[:slice_stop]" and return that 

189 partial result. This is needed for scrolling the window correctly 

190 when line wrapping. 

191 :returns: The computed height. 

192 """ 

193 # Instead of using `get_line_prefix` as key, we use render_counter 

194 # instead. This is more reliable, because this function could still be 

195 # the same, while the content would change over time. 

196 key = get_app().render_counter, lineno, width, slice_stop 

197 

198 try: 

199 return self._line_heights_cache[key] 

200 except KeyError: 

201 if width == 0: 

202 height = 10**8 

203 else: 

204 # Calculate line width first. 

205 line = fragment_list_to_text(self.get_line(lineno))[:slice_stop] 

206 text_width = get_cwidth(line) 

207 

208 if get_line_prefix: 

209 # Add prefix width. 

210 text_width += fragment_list_width( 

211 to_formatted_text(get_line_prefix(lineno, 0)) 

212 ) 

213 

214 # Slower path: compute path when there's a line prefix. 

215 height = 1 

216 

217 # Keep wrapping as long as the line doesn't fit. 

218 # Keep adding new prefixes for every wrapped line. 

219 while text_width > width: 

220 height += 1 

221 text_width -= width 

222 

223 fragments2 = to_formatted_text( 

224 get_line_prefix(lineno, height - 1) 

225 ) 

226 prefix_width = get_cwidth(fragment_list_to_text(fragments2)) 

227 

228 if prefix_width >= width: # Prefix doesn't fit. 

229 height = 10**8 

230 break 

231 

232 text_width += prefix_width 

233 else: 

234 # Fast path: compute height when there's no line prefix. 

235 try: 

236 quotient, remainder = divmod(text_width, width) 

237 except ZeroDivisionError: 

238 height = 10**8 

239 else: 

240 if remainder: 

241 quotient += 1 # Like math.ceil. 

242 height = max(1, quotient) 

243 

244 # Cache and return 

245 self._line_heights_cache[key] = height 

246 return height 

247 

248 

249class FormattedTextControl(UIControl): 

250 """ 

251 Control that displays formatted text. This can be either plain text, an 

252 :class:`~prompt_toolkit.formatted_text.HTML` object an 

253 :class:`~prompt_toolkit.formatted_text.ANSI` object, a list of ``(style_str, 

254 text)`` tuples or a callable that takes no argument and returns one of 

255 those, depending on how you prefer to do the formatting. See 

256 ``prompt_toolkit.layout.formatted_text`` for more information. 

257 

258 (It's mostly optimized for rather small widgets, like toolbars, menus, etc...) 

259 

260 When this UI control has the focus, the cursor will be shown in the upper 

261 left corner of this control by default. There are two ways for specifying 

262 the cursor position: 

263 

264 - Pass a `get_cursor_position` function which returns a `Point` instance 

265 with the current cursor position. 

266 

267 - If the (formatted) text is passed as a list of ``(style, text)`` tuples 

268 and there is one that looks like ``('[SetCursorPosition]', '')``, then 

269 this will specify the cursor position. 

270 

271 Mouse support: 

272 

273 The list of fragments can also contain tuples of three items, looking like: 

274 (style_str, text, handler). When mouse support is enabled and the user 

275 clicks on this fragment, then the given handler is called. That handler 

276 should accept two inputs: (Application, MouseEvent) and it should 

277 either handle the event or return `NotImplemented` in case we want the 

278 containing Window to handle this event. 

279 

280 :param focusable: `bool` or :class:`.Filter`: Tell whether this control is 

281 focusable. 

282 

283 :param text: Text or formatted text to be displayed. 

284 :param style: Style string applied to the content. (If you want to style 

285 the whole :class:`~prompt_toolkit.layout.Window`, pass the style to the 

286 :class:`~prompt_toolkit.layout.Window` instead.) 

287 :param key_bindings: a :class:`.KeyBindings` object. 

288 :param get_cursor_position: A callable that returns the cursor position as 

289 a `Point` instance. 

290 """ 

291 

292 def __init__( 

293 self, 

294 text: AnyFormattedText = "", 

295 style: str = "", 

296 focusable: FilterOrBool = False, 

297 key_bindings: KeyBindingsBase | None = None, 

298 show_cursor: bool = True, 

299 modal: bool = False, 

300 get_cursor_position: Callable[[], Point | None] | None = None, 

301 ) -> None: 

302 self.text = text # No type check on 'text'. This is done dynamically. 

303 self.style = style 

304 self.focusable = to_filter(focusable) 

305 

306 # Key bindings. 

307 self.key_bindings = key_bindings 

308 self.show_cursor = show_cursor 

309 self.modal = modal 

310 self.get_cursor_position = get_cursor_position 

311 

312 #: Cache for the content. 

313 self._content_cache: SimpleCache[Hashable, UIContent] = SimpleCache(maxsize=18) 

314 self._fragment_cache: SimpleCache[int, StyleAndTextTuples] = SimpleCache( 

315 maxsize=1 

316 ) 

317 # Only cache one fragment list. We don't need the previous item. 

318 

319 # Render info for the mouse support. 

320 self._fragments: StyleAndTextTuples | None = None 

321 

322 def reset(self) -> None: 

323 self._fragments = None 

324 

325 def is_focusable(self) -> bool: 

326 return self.focusable() 

327 

328 def __repr__(self) -> str: 

329 return f"{self.__class__.__name__}({self.text!r})" 

330 

331 def _get_formatted_text_cached(self) -> StyleAndTextTuples: 

332 """ 

333 Get fragments, but only retrieve fragments once during one render run. 

334 (This function is called several times during one rendering, because 

335 we also need those for calculating the dimensions.) 

336 """ 

337 return self._fragment_cache.get( 

338 get_app().render_counter, lambda: to_formatted_text(self.text, self.style) 

339 ) 

340 

341 def preferred_width(self, max_available_width: int) -> int: 

342 """ 

343 Return the preferred width for this control. 

344 That is the width of the longest line. 

345 """ 

346 text = fragment_list_to_text(self._get_formatted_text_cached()) 

347 line_lengths = [get_cwidth(l) for l in text.split("\n")] 

348 return max(line_lengths) 

349 

350 def preferred_height( 

351 self, 

352 width: int, 

353 max_available_height: int, 

354 wrap_lines: bool, 

355 get_line_prefix: GetLinePrefixCallable | None, 

356 ) -> int | None: 

357 """ 

358 Return the preferred height for this control. 

359 """ 

360 content = self.create_content(width, None) 

361 if wrap_lines: 

362 height = 0 

363 for i in range(content.line_count): 

364 height += content.get_height_for_line(i, width, get_line_prefix) 

365 if height >= max_available_height: 

366 return max_available_height 

367 return height 

368 else: 

369 return content.line_count 

370 

371 def create_content(self, width: int, height: int | None) -> UIContent: 

372 # Get fragments 

373 fragments_with_mouse_handlers = self._get_formatted_text_cached() 

374 fragment_lines_with_mouse_handlers = list( 

375 split_lines(fragments_with_mouse_handlers) 

376 ) 

377 

378 # Strip mouse handlers from fragments. 

379 fragment_lines: list[StyleAndTextTuples] = [ 

380 [(item[0], item[1]) for item in line] 

381 for line in fragment_lines_with_mouse_handlers 

382 ] 

383 

384 # Keep track of the fragments with mouse handler, for later use in 

385 # `mouse_handler`. 

386 self._fragments = fragments_with_mouse_handlers 

387 

388 # If there is a `[SetCursorPosition]` in the fragment list, set the 

389 # cursor position here. 

390 def get_cursor_position( 

391 fragment: str = "[SetCursorPosition]", 

392 ) -> Point | None: 

393 for y, line in enumerate(fragment_lines): 

394 x = 0 

395 for style_str, text, *_ in line: 

396 if fragment in style_str: 

397 return Point(x=x, y=y) 

398 x += len(text) 

399 return None 

400 

401 # If there is a `[SetMenuPosition]`, set the menu over here. 

402 def get_menu_position() -> Point | None: 

403 return get_cursor_position("[SetMenuPosition]") 

404 

405 cursor_position = (self.get_cursor_position or get_cursor_position)() 

406 

407 # Create content, or take it from the cache. 

408 key = (tuple(fragments_with_mouse_handlers), width, cursor_position) 

409 

410 def get_content() -> UIContent: 

411 return UIContent( 

412 get_line=lambda i: fragment_lines[i], 

413 line_count=len(fragment_lines), 

414 show_cursor=self.show_cursor, 

415 cursor_position=cursor_position, 

416 menu_position=get_menu_position(), 

417 ) 

418 

419 return self._content_cache.get(key, get_content) 

420 

421 def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: 

422 """ 

423 Handle mouse events. 

424 

425 (When the fragment list contained mouse handlers and the user clicked on 

426 on any of these, the matching handler is called. This handler can still 

427 return `NotImplemented` in case we want the 

428 :class:`~prompt_toolkit.layout.Window` to handle this particular 

429 event.) 

430 """ 

431 if self._fragments: 

432 # Read the generator. 

433 fragments_for_line = list(split_lines(self._fragments)) 

434 

435 try: 

436 fragments = fragments_for_line[mouse_event.position.y] 

437 except IndexError: 

438 return NotImplemented 

439 else: 

440 # Find position in the fragment list. 

441 xpos = mouse_event.position.x 

442 

443 # Find mouse handler for this character. 

444 count = 0 

445 for item in fragments: 

446 count += len(item[1]) 

447 if count > xpos: 

448 if len(item) >= 3: 

449 # Handler found. Call it. 

450 # (Handler can return NotImplemented, so return 

451 # that result.) 

452 handler = item[2] 

453 return handler(mouse_event) 

454 else: 

455 break 

456 

457 # Otherwise, don't handle here. 

458 return NotImplemented 

459 

460 def is_modal(self) -> bool: 

461 return self.modal 

462 

463 def get_key_bindings(self) -> KeyBindingsBase | None: 

464 return self.key_bindings 

465 

466 

467class DummyControl(UIControl): 

468 """ 

469 A dummy control object that doesn't paint any content. 

470 

471 Useful for filling a :class:`~prompt_toolkit.layout.Window`. (The 

472 `fragment` and `char` attributes of the `Window` class can be used to 

473 define the filling.) 

474 """ 

475 

476 def create_content(self, width: int, height: int) -> UIContent: 

477 def get_line(i: int) -> StyleAndTextTuples: 

478 return [] 

479 

480 return UIContent(get_line=get_line, line_count=100**100) # Something very big. 

481 

482 def is_focusable(self) -> bool: 

483 return False 

484 

485 

486class _ProcessedLine(NamedTuple): 

487 fragments: StyleAndTextTuples 

488 source_to_display: Callable[[int], int] 

489 display_to_source: Callable[[int], int] 

490 

491 

492class BufferControl(UIControl): 

493 """ 

494 Control for visualizing the content of a :class:`.Buffer`. 

495 

496 :param buffer: The :class:`.Buffer` object to be displayed. 

497 :param input_processors: A list of 

498 :class:`~prompt_toolkit.layout.processors.Processor` objects. 

499 :param include_default_input_processors: When True, include the default 

500 processors for highlighting of selection, search and displaying of 

501 multiple cursors. 

502 :param lexer: :class:`.Lexer` instance for syntax highlighting. 

503 :param preview_search: `bool` or :class:`.Filter`: Show search while 

504 typing. When this is `True`, probably you want to add a 

505 ``HighlightIncrementalSearchProcessor`` as well. Otherwise only the 

506 cursor position will move, but the text won't be highlighted. 

507 :param focusable: `bool` or :class:`.Filter`: Tell whether this control is focusable. 

508 :param focus_on_click: Focus this buffer when it's click, but not yet focused. 

509 :param key_bindings: a :class:`.KeyBindings` object. 

510 """ 

511 

512 def __init__( 

513 self, 

514 buffer: Buffer | None = None, 

515 input_processors: list[Processor] | None = None, 

516 include_default_input_processors: bool = True, 

517 lexer: Lexer | None = None, 

518 preview_search: FilterOrBool = False, 

519 focusable: FilterOrBool = True, 

520 search_buffer_control: ( 

521 None | SearchBufferControl | Callable[[], SearchBufferControl] 

522 ) = None, 

523 menu_position: Callable[[], int | None] | None = None, 

524 focus_on_click: FilterOrBool = False, 

525 key_bindings: KeyBindingsBase | None = None, 

526 ): 

527 self.input_processors = input_processors 

528 self.include_default_input_processors = include_default_input_processors 

529 

530 self.default_input_processors = [ 

531 HighlightSearchProcessor(), 

532 HighlightIncrementalSearchProcessor(), 

533 HighlightSelectionProcessor(), 

534 DisplayMultipleCursors(), 

535 ] 

536 

537 self.preview_search = to_filter(preview_search) 

538 self.focusable = to_filter(focusable) 

539 self.focus_on_click = to_filter(focus_on_click) 

540 

541 self.buffer = buffer or Buffer() 

542 self.menu_position = menu_position 

543 self.lexer = lexer or SimpleLexer() 

544 self.key_bindings = key_bindings 

545 self._search_buffer_control = search_buffer_control 

546 

547 #: Cache for the lexer. 

548 #: Often, due to cursor movement, undo/redo and window resizing 

549 #: operations, it happens that a short time, the same document has to be 

550 #: lexed. This is a fairly easy way to cache such an expensive operation. 

551 self._fragment_cache: SimpleCache[ 

552 Hashable, Callable[[int], StyleAndTextTuples] 

553 ] = SimpleCache(maxsize=8) 

554 

555 self._last_click_timestamp: float | None = None 

556 self._last_get_processed_line: Callable[[int], _ProcessedLine] | None = None 

557 

558 def __repr__(self) -> str: 

559 return f"<{self.__class__.__name__} buffer={self.buffer!r} at {id(self)!r}>" 

560 

561 @property 

562 def search_buffer_control(self) -> SearchBufferControl | None: 

563 result: SearchBufferControl | None 

564 

565 if callable(self._search_buffer_control): 

566 result = self._search_buffer_control() 

567 else: 

568 result = self._search_buffer_control 

569 

570 assert result is None or isinstance(result, SearchBufferControl) 

571 return result 

572 

573 @property 

574 def search_buffer(self) -> Buffer | None: 

575 control = self.search_buffer_control 

576 if control is not None: 

577 return control.buffer 

578 return None 

579 

580 @property 

581 def search_state(self) -> SearchState: 

582 """ 

583 Return the `SearchState` for searching this `BufferControl`. This is 

584 always associated with the search control. If one search bar is used 

585 for searching multiple `BufferControls`, then they share the same 

586 `SearchState`. 

587 """ 

588 search_buffer_control = self.search_buffer_control 

589 if search_buffer_control: 

590 return search_buffer_control.searcher_search_state 

591 else: 

592 return SearchState() 

593 

594 def is_focusable(self) -> bool: 

595 return self.focusable() 

596 

597 def preferred_width(self, max_available_width: int) -> int | None: 

598 """ 

599 This should return the preferred width. 

600 

601 Note: We don't specify a preferred width according to the content, 

602 because it would be too expensive. Calculating the preferred 

603 width can be done by calculating the longest line, but this would 

604 require applying all the processors to each line. This is 

605 unfeasible for a larger document, and doing it for small 

606 documents only would result in inconsistent behavior. 

607 """ 

608 return None 

609 

610 def preferred_height( 

611 self, 

612 width: int, 

613 max_available_height: int, 

614 wrap_lines: bool, 

615 get_line_prefix: GetLinePrefixCallable | None, 

616 ) -> int | None: 

617 # Calculate the content height, if it was drawn on a screen with the 

618 # given width. 

619 height = 0 

620 content = self.create_content(width, height=1) # Pass a dummy '1' as height. 

621 

622 # When line wrapping is off, the height should be equal to the amount 

623 # of lines. 

624 if not wrap_lines: 

625 return content.line_count 

626 

627 # When the number of lines exceeds the max_available_height, just 

628 # return max_available_height. No need to calculate anything. 

629 if content.line_count >= max_available_height: 

630 return max_available_height 

631 

632 for i in range(content.line_count): 

633 height += content.get_height_for_line(i, width, get_line_prefix) 

634 

635 if height >= max_available_height: 

636 return max_available_height 

637 

638 return height 

639 

640 def _get_formatted_text_for_line_func( 

641 self, document: Document 

642 ) -> Callable[[int], StyleAndTextTuples]: 

643 """ 

644 Create a function that returns the fragments for a given line. 

645 """ 

646 

647 # Cache using `document.text`. 

648 def get_formatted_text_for_line() -> Callable[[int], StyleAndTextTuples]: 

649 return self.lexer.lex_document(document) 

650 

651 key = (document.text, self.lexer.invalidation_hash()) 

652 return self._fragment_cache.get(key, get_formatted_text_for_line) 

653 

654 def _create_get_processed_line_func( 

655 self, document: Document, width: int, height: int 

656 ) -> Callable[[int], _ProcessedLine]: 

657 """ 

658 Create a function that takes a line number of the current document and 

659 returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source) 

660 tuple. 

661 """ 

662 # Merge all input processors together. 

663 input_processors = self.input_processors or [] 

664 if self.include_default_input_processors: 

665 input_processors = self.default_input_processors + input_processors 

666 

667 merged_processor = merge_processors(input_processors) 

668 

669 def transform(lineno: int, fragments: StyleAndTextTuples) -> _ProcessedLine: 

670 "Transform the fragments for a given line number." 

671 

672 # Get cursor position at this line. 

673 def source_to_display(i: int) -> int: 

674 """X position from the buffer to the x position in the 

675 processed fragment list. By default, we start from the 'identity' 

676 operation.""" 

677 return i 

678 

679 transformation = merged_processor.apply_transformation( 

680 TransformationInput( 

681 self, document, lineno, source_to_display, fragments, width, height 

682 ) 

683 ) 

684 

685 return _ProcessedLine( 

686 transformation.fragments, 

687 transformation.source_to_display, 

688 transformation.display_to_source, 

689 ) 

690 

691 def create_func() -> Callable[[int], _ProcessedLine]: 

692 get_line = self._get_formatted_text_for_line_func(document) 

693 cache: dict[int, _ProcessedLine] = {} 

694 

695 def get_processed_line(i: int) -> _ProcessedLine: 

696 try: 

697 return cache[i] 

698 except KeyError: 

699 processed_line = transform(i, get_line(i)) 

700 cache[i] = processed_line 

701 return processed_line 

702 

703 return get_processed_line 

704 

705 return create_func() 

706 

707 def create_content( 

708 self, width: int, height: int, preview_search: bool = False 

709 ) -> UIContent: 

710 """ 

711 Create a UIContent. 

712 """ 

713 buffer = self.buffer 

714 

715 # Trigger history loading of the buffer. We do this during the 

716 # rendering of the UI here, because it needs to happen when an 

717 # `Application` with its event loop is running. During the rendering of 

718 # the buffer control is the earliest place we can achieve this, where 

719 # we're sure the right event loop is active, and don't require user 

720 # interaction (like in a key binding). 

721 buffer.load_history_if_not_yet_loaded() 

722 

723 # Get the document to be shown. If we are currently searching (the 

724 # search buffer has focus, and the preview_search filter is enabled), 

725 # then use the search document, which has possibly a different 

726 # text/cursor position.) 

727 search_control = self.search_buffer_control 

728 preview_now = preview_search or bool( 

729 # Only if this feature is enabled. 

730 self.preview_search() 

731 and 

732 # And something was typed in the associated search field. 

733 search_control 

734 and search_control.buffer.text 

735 and 

736 # And we are searching in this control. (Many controls can point to 

737 # the same search field, like in Pyvim.) 

738 get_app().layout.search_target_buffer_control == self 

739 ) 

740 

741 if preview_now and search_control is not None: 

742 ss = self.search_state 

743 

744 document = buffer.document_for_search( 

745 SearchState( 

746 text=search_control.buffer.text, 

747 direction=ss.direction, 

748 ignore_case=ss.ignore_case, 

749 ) 

750 ) 

751 else: 

752 document = buffer.document 

753 

754 get_processed_line = self._create_get_processed_line_func( 

755 document, width, height 

756 ) 

757 self._last_get_processed_line = get_processed_line 

758 

759 def translate_rowcol(row: int, col: int) -> Point: 

760 "Return the content column for this coordinate." 

761 return Point(x=get_processed_line(row).source_to_display(col), y=row) 

762 

763 def get_line(i: int) -> StyleAndTextTuples: 

764 "Return the fragments for a given line number." 

765 fragments = get_processed_line(i).fragments 

766 

767 # Add a space at the end, because that is a possible cursor 

768 # position. (When inserting after the input.) We should do this on 

769 # all the lines, not just the line containing the cursor. (Because 

770 # otherwise, line wrapping/scrolling could change when moving the 

771 # cursor around.) 

772 fragments = fragments + [("", " ")] 

773 return fragments 

774 

775 content = UIContent( 

776 get_line=get_line, 

777 line_count=document.line_count, 

778 cursor_position=translate_rowcol( 

779 document.cursor_position_row, document.cursor_position_col 

780 ), 

781 ) 

782 

783 # If there is an auto completion going on, use that start point for a 

784 # pop-up menu position. (But only when this buffer has the focus -- 

785 # there is only one place for a menu, determined by the focused buffer.) 

786 if get_app().layout.current_control == self: 

787 menu_position = self.menu_position() if self.menu_position else None 

788 if menu_position is not None: 

789 assert isinstance(menu_position, int) 

790 menu_row, menu_col = buffer.document.translate_index_to_position( 

791 menu_position 

792 ) 

793 content.menu_position = translate_rowcol(menu_row, menu_col) 

794 elif buffer.complete_state: 

795 # Position for completion menu. 

796 # Note: We use 'min', because the original cursor position could be 

797 # behind the input string when the actual completion is for 

798 # some reason shorter than the text we had before. (A completion 

799 # can change and shorten the input.) 

800 menu_row, menu_col = buffer.document.translate_index_to_position( 

801 min( 

802 buffer.cursor_position, 

803 buffer.complete_state.original_document.cursor_position, 

804 ) 

805 ) 

806 content.menu_position = translate_rowcol(menu_row, menu_col) 

807 else: 

808 content.menu_position = None 

809 

810 return content 

811 

812 def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: 

813 """ 

814 Mouse handler for this control. 

815 """ 

816 buffer = self.buffer 

817 position = mouse_event.position 

818 

819 # Focus buffer when clicked. 

820 if get_app().layout.current_control == self: 

821 if self._last_get_processed_line: 

822 processed_line = self._last_get_processed_line(position.y) 

823 

824 # Translate coordinates back to the cursor position of the 

825 # original input. 

826 xpos = processed_line.display_to_source(position.x) 

827 index = buffer.document.translate_row_col_to_index(position.y, xpos) 

828 

829 # Set the cursor position. 

830 if mouse_event.event_type == MouseEventType.MOUSE_DOWN: 

831 buffer.exit_selection() 

832 buffer.cursor_position = index 

833 

834 elif ( 

835 mouse_event.event_type == MouseEventType.MOUSE_MOVE 

836 and mouse_event.button != MouseButton.NONE 

837 ): 

838 # Click and drag to highlight a selection 

839 if ( 

840 buffer.selection_state is None 

841 and abs(buffer.cursor_position - index) > 0 

842 ): 

843 buffer.start_selection(selection_type=SelectionType.CHARACTERS) 

844 buffer.cursor_position = index 

845 

846 elif mouse_event.event_type == MouseEventType.MOUSE_UP: 

847 # When the cursor was moved to another place, select the text. 

848 # (The >1 is actually a small but acceptable workaround for 

849 # selecting text in Vi navigation mode. In navigation mode, 

850 # the cursor can never be after the text, so the cursor 

851 # will be repositioned automatically.) 

852 if abs(buffer.cursor_position - index) > 1: 

853 if buffer.selection_state is None: 

854 buffer.start_selection( 

855 selection_type=SelectionType.CHARACTERS 

856 ) 

857 buffer.cursor_position = index 

858 

859 # Select word around cursor on double click. 

860 # Two MOUSE_UP events in a short timespan are considered a double click. 

861 double_click = ( 

862 self._last_click_timestamp 

863 and time.time() - self._last_click_timestamp < 0.3 

864 ) 

865 self._last_click_timestamp = time.time() 

866 

867 if double_click: 

868 start, end = buffer.document.find_boundaries_of_current_word() 

869 buffer.cursor_position += start 

870 buffer.start_selection(selection_type=SelectionType.CHARACTERS) 

871 buffer.cursor_position += end - start 

872 else: 

873 # Don't handle scroll events here. 

874 return NotImplemented 

875 

876 # Not focused, but focusing on click events. 

877 else: 

878 if ( 

879 self.focus_on_click() 

880 and mouse_event.event_type == MouseEventType.MOUSE_UP 

881 ): 

882 # Focus happens on mouseup. (If we did this on mousedown, the 

883 # up event will be received at the point where this widget is 

884 # focused and be handled anyway.) 

885 get_app().layout.current_control = self 

886 else: 

887 return NotImplemented 

888 

889 return None 

890 

891 def move_cursor_down(self) -> None: 

892 b = self.buffer 

893 b.cursor_position += b.document.get_cursor_down_position() 

894 

895 def move_cursor_up(self) -> None: 

896 b = self.buffer 

897 b.cursor_position += b.document.get_cursor_up_position() 

898 

899 def get_key_bindings(self) -> KeyBindingsBase | None: 

900 """ 

901 When additional key bindings are given. Return these. 

902 """ 

903 return self.key_bindings 

904 

905 def get_invalidate_events(self) -> Iterable[Event[object]]: 

906 """ 

907 Return the Window invalidate events. 

908 """ 

909 # Whenever the buffer changes, the UI has to be updated. 

910 yield self.buffer.on_text_changed 

911 yield self.buffer.on_cursor_position_changed 

912 

913 yield self.buffer.on_completions_changed 

914 yield self.buffer.on_suggestion_set 

915 

916 

917class SearchBufferControl(BufferControl): 

918 """ 

919 :class:`.BufferControl` which is used for searching another 

920 :class:`.BufferControl`. 

921 

922 :param ignore_case: Search case insensitive. 

923 """ 

924 

925 def __init__( 

926 self, 

927 buffer: Buffer | None = None, 

928 input_processors: list[Processor] | None = None, 

929 lexer: Lexer | None = None, 

930 focus_on_click: FilterOrBool = False, 

931 key_bindings: KeyBindingsBase | None = None, 

932 ignore_case: FilterOrBool = False, 

933 ): 

934 super().__init__( 

935 buffer=buffer, 

936 input_processors=input_processors, 

937 lexer=lexer, 

938 focus_on_click=focus_on_click, 

939 key_bindings=key_bindings, 

940 ) 

941 

942 # If this BufferControl is used as a search field for one or more other 

943 # BufferControls, then represents the search state. 

944 self.searcher_search_state = SearchState(ignore_case=ignore_case)