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

327 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:07 +0000

1""" 

2User interface Controls for the layout. 

3""" 

4from __future__ import annotations 

5 

6import time 

7from abc import ABCMeta, abstractmethod 

8from typing import ( 

9 TYPE_CHECKING, 

10 Callable, 

11 Dict, 

12 Hashable, 

13 Iterable, 

14 List, 

15 NamedTuple, 

16 Optional, 

17 Union, 

18) 

19 

20from prompt_toolkit.application.current import get_app 

21from prompt_toolkit.buffer import Buffer 

22from prompt_toolkit.cache import SimpleCache 

23from prompt_toolkit.data_structures import Point 

24from prompt_toolkit.document import Document 

25from prompt_toolkit.filters import FilterOrBool, to_filter 

26from prompt_toolkit.formatted_text import ( 

27 AnyFormattedText, 

28 StyleAndTextTuples, 

29 to_formatted_text, 

30) 

31from prompt_toolkit.formatted_text.utils import ( 

32 fragment_list_to_text, 

33 fragment_list_width, 

34 split_lines, 

35) 

36from prompt_toolkit.lexers import Lexer, SimpleLexer 

37from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType 

38from prompt_toolkit.search import SearchState 

39from prompt_toolkit.selection import SelectionType 

40from prompt_toolkit.utils import get_cwidth 

41 

42from .processors import ( 

43 DisplayMultipleCursors, 

44 HighlightIncrementalSearchProcessor, 

45 HighlightSearchProcessor, 

46 HighlightSelectionProcessor, 

47 Processor, 

48 TransformationInput, 

49 merge_processors, 

50) 

51 

52if TYPE_CHECKING: 

53 from prompt_toolkit.key_binding.key_bindings import ( 

54 KeyBindingsBase, 

55 NotImplementedOrNone, 

56 ) 

57 from prompt_toolkit.utils import Event 

58 

59 

60__all__ = [ 

61 "BufferControl", 

62 "SearchBufferControl", 

63 "DummyControl", 

64 "FormattedTextControl", 

65 "UIControl", 

66 "UIContent", 

67] 

68 

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

70 

71 

72class UIControl(metaclass=ABCMeta): 

73 """ 

74 Base class for all user interface controls. 

75 """ 

76 

77 def reset(self) -> None: 

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

79 pass 

80 

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

82 return None 

83 

84 def preferred_height( 

85 self, 

86 width: int, 

87 max_available_height: int, 

88 wrap_lines: bool, 

89 get_line_prefix: GetLinePrefixCallable | None, 

90 ) -> int | None: 

91 return None 

92 

93 def is_focusable(self) -> bool: 

94 """ 

95 Tell whether this user control is focusable. 

96 """ 

97 return False 

98 

99 @abstractmethod 

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

101 """ 

102 Generate the content for this user control. 

103 

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

105 """ 

106 

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

108 """ 

109 Handle mouse events. 

110 

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

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

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

114 

115 :param mouse_event: `MouseEvent` instance. 

116 """ 

117 return NotImplemented 

118 

119 def move_cursor_down(self) -> None: 

120 """ 

121 Request to move the cursor down. 

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

123 top. 

124 """ 

125 

126 def move_cursor_up(self) -> None: 

127 """ 

128 Request to move the cursor up. 

129 """ 

130 

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

132 """ 

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

134 

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

136 specified, or `None` otherwise. 

137 """ 

138 

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

140 """ 

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

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

143 handlers to these events.) 

144 """ 

145 return [] 

146 

147 

148class UIContent: 

149 """ 

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

151 lines. 

152 

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

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

155 :param line_count: The number of lines. 

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

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

158 :param show_cursor: Make the cursor visible. 

159 """ 

160 

161 def __init__( 

162 self, 

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

164 line_count: int = 0, 

165 cursor_position: Point | None = None, 

166 menu_position: Point | None = None, 

167 show_cursor: bool = True, 

168 ): 

169 self.get_line = get_line 

170 self.line_count = line_count 

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

172 self.menu_position = menu_position 

173 self.show_cursor = show_cursor 

174 

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

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

177 

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

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

180 if lineno < self.line_count: 

181 return self.get_line(lineno) 

182 else: 

183 raise IndexError 

184 

185 def get_height_for_line( 

186 self, 

187 lineno: int, 

188 width: int, 

189 get_line_prefix: GetLinePrefixCallable | None, 

190 slice_stop: int | None = None, 

191 ) -> int: 

192 """ 

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

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

195 

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

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

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

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

200 when line wrapping. 

201 :returns: The computed height. 

202 """ 

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

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

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

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

207 

208 try: 

209 return self._line_heights_cache[key] 

210 except KeyError: 

211 if width == 0: 

212 height = 10**8 

213 else: 

214 # Calculate line width first. 

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

216 text_width = get_cwidth(line) 

217 

218 if get_line_prefix: 

219 # Add prefix width. 

220 text_width += fragment_list_width( 

221 to_formatted_text(get_line_prefix(lineno, 0)) 

222 ) 

223 

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

225 height = 1 

226 

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

228 # Keep adding new prefixes for every wrapped line. 

229 while text_width > width: 

230 height += 1 

231 text_width -= width 

232 

233 fragments2 = to_formatted_text( 

234 get_line_prefix(lineno, height - 1) 

235 ) 

236 prefix_width = get_cwidth(fragment_list_to_text(fragments2)) 

237 

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

239 height = 10**8 

240 break 

241 

242 text_width += prefix_width 

243 else: 

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

245 try: 

246 quotient, remainder = divmod(text_width, width) 

247 except ZeroDivisionError: 

248 height = 10**8 

249 else: 

250 if remainder: 

251 quotient += 1 # Like math.ceil. 

252 height = max(1, quotient) 

253 

254 # Cache and return 

255 self._line_heights_cache[key] = height 

256 return height 

257 

258 

259class FormattedTextControl(UIControl): 

260 """ 

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

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

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

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

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

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

267 

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

269 

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

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

272 the cursor position: 

273 

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

275 with the current cursor position. 

276 

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

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

279 this will specify the cursor position. 

280 

281 Mouse support: 

282 

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

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

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

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

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

288 containing Window to handle this event. 

289 

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

291 focusable. 

292 

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

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

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

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

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

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

299 a `Point` instance. 

300 """ 

301 

302 def __init__( 

303 self, 

304 text: AnyFormattedText = "", 

305 style: str = "", 

306 focusable: FilterOrBool = False, 

307 key_bindings: KeyBindingsBase | None = None, 

308 show_cursor: bool = True, 

309 modal: bool = False, 

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

311 ) -> None: 

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

313 self.style = style 

314 self.focusable = to_filter(focusable) 

315 

316 # Key bindings. 

317 self.key_bindings = key_bindings 

318 self.show_cursor = show_cursor 

319 self.modal = modal 

320 self.get_cursor_position = get_cursor_position 

321 

322 #: Cache for the content. 

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

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

325 maxsize=1 

326 ) 

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

328 

329 # Render info for the mouse support. 

330 self._fragments: StyleAndTextTuples | None = None 

331 

332 def reset(self) -> None: 

333 self._fragments = None 

334 

335 def is_focusable(self) -> bool: 

336 return self.focusable() 

337 

338 def __repr__(self) -> str: 

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

340 

341 def _get_formatted_text_cached(self) -> StyleAndTextTuples: 

342 """ 

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

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

345 we also need those for calculating the dimensions.) 

346 """ 

347 return self._fragment_cache.get( 

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

349 ) 

350 

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

352 """ 

353 Return the preferred width for this control. 

354 That is the width of the longest line. 

355 """ 

356 text = fragment_list_to_text(self._get_formatted_text_cached()) 

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

358 return max(line_lengths) 

359 

360 def preferred_height( 

361 self, 

362 width: int, 

363 max_available_height: int, 

364 wrap_lines: bool, 

365 get_line_prefix: GetLinePrefixCallable | None, 

366 ) -> int | None: 

367 """ 

368 Return the preferred height for this control. 

369 """ 

370 content = self.create_content(width, None) 

371 if wrap_lines: 

372 height = 0 

373 for i in range(content.line_count): 

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

375 if height >= max_available_height: 

376 return max_available_height 

377 return height 

378 else: 

379 return content.line_count 

380 

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

382 # Get fragments 

383 fragments_with_mouse_handlers = self._get_formatted_text_cached() 

384 fragment_lines_with_mouse_handlers = list( 

385 split_lines(fragments_with_mouse_handlers) 

386 ) 

387 

388 # Strip mouse handlers from fragments. 

389 fragment_lines: list[StyleAndTextTuples] = [ 

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

391 for line in fragment_lines_with_mouse_handlers 

392 ] 

393 

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

395 # `mouse_handler`. 

396 self._fragments = fragments_with_mouse_handlers 

397 

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

399 # cursor position here. 

400 def get_cursor_position( 

401 fragment: str = "[SetCursorPosition]", 

402 ) -> Point | None: 

403 for y, line in enumerate(fragment_lines): 

404 x = 0 

405 for style_str, text, *_ in line: 

406 if fragment in style_str: 

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

408 x += len(text) 

409 return None 

410 

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

412 def get_menu_position() -> Point | None: 

413 return get_cursor_position("[SetMenuPosition]") 

414 

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

416 

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

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

419 

420 def get_content() -> UIContent: 

421 return UIContent( 

422 get_line=lambda i: fragment_lines[i], 

423 line_count=len(fragment_lines), 

424 show_cursor=self.show_cursor, 

425 cursor_position=cursor_position, 

426 menu_position=get_menu_position(), 

427 ) 

428 

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

430 

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

432 """ 

433 Handle mouse events. 

434 

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

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

437 return `NotImplemented` in case we want the 

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

439 event.) 

440 """ 

441 if self._fragments: 

442 # Read the generator. 

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

444 

445 try: 

446 fragments = fragments_for_line[mouse_event.position.y] 

447 except IndexError: 

448 return NotImplemented 

449 else: 

450 # Find position in the fragment list. 

451 xpos = mouse_event.position.x 

452 

453 # Find mouse handler for this character. 

454 count = 0 

455 for item in fragments: 

456 count += len(item[1]) 

457 if count > xpos: 

458 if len(item) >= 3: 

459 # Handler found. Call it. 

460 # (Handler can return NotImplemented, so return 

461 # that result.) 

462 handler = item[2] # type: ignore 

463 return handler(mouse_event) 

464 else: 

465 break 

466 

467 # Otherwise, don't handle here. 

468 return NotImplemented 

469 

470 def is_modal(self) -> bool: 

471 return self.modal 

472 

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

474 return self.key_bindings 

475 

476 

477class DummyControl(UIControl): 

478 """ 

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

480 

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

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

483 define the filling.) 

484 """ 

485 

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

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

488 return [] 

489 

490 return UIContent( 

491 get_line=get_line, line_count=100**100 

492 ) # Something very big. 

493 

494 def is_focusable(self) -> bool: 

495 return False 

496 

497 

498class _ProcessedLine(NamedTuple): 

499 fragments: StyleAndTextTuples 

500 source_to_display: Callable[[int], int] 

501 display_to_source: Callable[[int], int] 

502 

503 

504class BufferControl(UIControl): 

505 """ 

506 Control for visualising the content of a :class:`.Buffer`. 

507 

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

509 :param input_processors: A list of 

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

511 :param include_default_input_processors: When True, include the default 

512 processors for highlighting of selection, search and displaying of 

513 multiple cursors. 

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

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

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

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

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

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

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

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

522 """ 

523 

524 def __init__( 

525 self, 

526 buffer: Buffer | None = None, 

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

528 include_default_input_processors: bool = True, 

529 lexer: Lexer | None = None, 

530 preview_search: FilterOrBool = False, 

531 focusable: FilterOrBool = True, 

532 search_buffer_control: ( 

533 None | SearchBufferControl | Callable[[], SearchBufferControl] 

534 ) = None, 

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

536 focus_on_click: FilterOrBool = False, 

537 key_bindings: KeyBindingsBase | None = None, 

538 ): 

539 self.input_processors = input_processors 

540 self.include_default_input_processors = include_default_input_processors 

541 

542 self.default_input_processors = [ 

543 HighlightSearchProcessor(), 

544 HighlightIncrementalSearchProcessor(), 

545 HighlightSelectionProcessor(), 

546 DisplayMultipleCursors(), 

547 ] 

548 

549 self.preview_search = to_filter(preview_search) 

550 self.focusable = to_filter(focusable) 

551 self.focus_on_click = to_filter(focus_on_click) 

552 

553 self.buffer = buffer or Buffer() 

554 self.menu_position = menu_position 

555 self.lexer = lexer or SimpleLexer() 

556 self.key_bindings = key_bindings 

557 self._search_buffer_control = search_buffer_control 

558 

559 #: Cache for the lexer. 

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

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

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

563 self._fragment_cache: SimpleCache[ 

564 Hashable, Callable[[int], StyleAndTextTuples] 

565 ] = SimpleCache(maxsize=8) 

566 

567 self._last_click_timestamp: float | None = None 

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

569 

570 def __repr__(self) -> str: 

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

572 

573 @property 

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

575 result: SearchBufferControl | None 

576 

577 if callable(self._search_buffer_control): 

578 result = self._search_buffer_control() 

579 else: 

580 result = self._search_buffer_control 

581 

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

583 return result 

584 

585 @property 

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

587 control = self.search_buffer_control 

588 if control is not None: 

589 return control.buffer 

590 return None 

591 

592 @property 

593 def search_state(self) -> SearchState: 

594 """ 

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

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

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

598 `SearchState`. 

599 """ 

600 search_buffer_control = self.search_buffer_control 

601 if search_buffer_control: 

602 return search_buffer_control.searcher_search_state 

603 else: 

604 return SearchState() 

605 

606 def is_focusable(self) -> bool: 

607 return self.focusable() 

608 

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

610 """ 

611 This should return the preferred width. 

612 

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

614 because it would be too expensive. Calculating the preferred 

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

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

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

618 documents only would result in inconsistent behaviour. 

619 """ 

620 return None 

621 

622 def preferred_height( 

623 self, 

624 width: int, 

625 max_available_height: int, 

626 wrap_lines: bool, 

627 get_line_prefix: GetLinePrefixCallable | None, 

628 ) -> int | None: 

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

630 # given width. 

631 height = 0 

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

633 

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

635 # of lines. 

636 if not wrap_lines: 

637 return content.line_count 

638 

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

640 # return max_available_height. No need to calculate anything. 

641 if content.line_count >= max_available_height: 

642 return max_available_height 

643 

644 for i in range(content.line_count): 

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

646 

647 if height >= max_available_height: 

648 return max_available_height 

649 

650 return height 

651 

652 def _get_formatted_text_for_line_func( 

653 self, document: Document 

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

655 """ 

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

657 """ 

658 

659 # Cache using `document.text`. 

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

661 return self.lexer.lex_document(document) 

662 

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

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

665 

666 def _create_get_processed_line_func( 

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

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

669 """ 

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

671 returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source) 

672 tuple. 

673 """ 

674 # Merge all input processors together. 

675 input_processors = self.input_processors or [] 

676 if self.include_default_input_processors: 

677 input_processors = self.default_input_processors + input_processors 

678 

679 merged_processor = merge_processors(input_processors) 

680 

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

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

683 

684 # Get cursor position at this line. 

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

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

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

688 operation.""" 

689 return i 

690 

691 transformation = merged_processor.apply_transformation( 

692 TransformationInput( 

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

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)) 

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)