Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/prompt_toolkit/layout/controls.py: 24%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

329 statements  

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)