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

328 statements  

1""" 

2User interface Controls for the layout. 

3""" 

4 

5from __future__ import annotations 

6 

7import time 

8from abc import ABCMeta, abstractmethod 

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

10 

11from prompt_toolkit.application.current import get_app 

12from prompt_toolkit.buffer import Buffer 

13from prompt_toolkit.cache import SimpleCache 

14from prompt_toolkit.data_structures import Point 

15from prompt_toolkit.document import Document 

16from prompt_toolkit.filters import FilterOrBool, to_filter 

17from prompt_toolkit.formatted_text import ( 

18 AnyFormattedText, 

19 StyleAndTextTuples, 

20 to_formatted_text, 

21) 

22from prompt_toolkit.formatted_text.utils import ( 

23 fragment_list_to_text, 

24 fragment_list_width, 

25 split_lines, 

26) 

27from prompt_toolkit.lexers import Lexer, SimpleLexer 

28from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType 

29from prompt_toolkit.search import SearchState 

30from prompt_toolkit.selection import SelectionType 

31from prompt_toolkit.utils import get_cwidth 

32 

33from .processors import ( 

34 DisplayMultipleCursors, 

35 HighlightIncrementalSearchProcessor, 

36 HighlightSearchProcessor, 

37 HighlightSelectionProcessor, 

38 Processor, 

39 TransformationInput, 

40 merge_processors, 

41) 

42 

43if TYPE_CHECKING: 

44 from prompt_toolkit.key_binding.key_bindings import ( 

45 KeyBindingsBase, 

46 NotImplementedOrNone, 

47 ) 

48 from prompt_toolkit.utils import Event 

49 

50 

51__all__ = [ 

52 "BufferControl", 

53 "SearchBufferControl", 

54 "DummyControl", 

55 "FormattedTextControl", 

56 "UIControl", 

57 "UIContent", 

58] 

59 

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

61 

62 

63class UIControl(metaclass=ABCMeta): 

64 """ 

65 Base class for all user interface controls. 

66 """ 

67 

68 def reset(self) -> None: 

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

70 pass 

71 

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

73 return None 

74 

75 def preferred_height( 

76 self, 

77 width: int, 

78 max_available_height: int, 

79 wrap_lines: bool, 

80 get_line_prefix: GetLinePrefixCallable | None, 

81 ) -> int | None: 

82 return None 

83 

84 def is_focusable(self) -> bool: 

85 """ 

86 Tell whether this user control is focusable. 

87 """ 

88 return False 

89 

90 @abstractmethod 

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

92 """ 

93 Generate the content for this user control. 

94 

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

96 """ 

97 

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

99 """ 

100 Handle mouse events. 

101 

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

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

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

105 

106 :param mouse_event: `MouseEvent` instance. 

107 """ 

108 return NotImplemented 

109 

110 def move_cursor_down(self) -> None: 

111 """ 

112 Request to move the cursor down. 

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

114 top. 

115 """ 

116 

117 def move_cursor_up(self) -> None: 

118 """ 

119 Request to move the cursor up. 

120 """ 

121 

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

123 """ 

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

125 

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

127 specified, or `None` otherwise. 

128 """ 

129 

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

131 """ 

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

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

134 handlers to these events.) 

135 """ 

136 return [] 

137 

138 

139class UIContent: 

140 """ 

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

142 lines. 

143 

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

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

146 :param line_count: The number of lines. 

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

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

149 :param show_cursor: Make the cursor visible. 

150 """ 

151 

152 def __init__( 

153 self, 

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

155 line_count: int = 0, 

156 cursor_position: Point | None = None, 

157 menu_position: Point | None = None, 

158 show_cursor: bool = True, 

159 ): 

160 self.get_line = get_line 

161 self.line_count = line_count 

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

163 self.menu_position = menu_position 

164 self.show_cursor = show_cursor 

165 

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

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

168 

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

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

171 if lineno < self.line_count: 

172 return self.get_line(lineno) 

173 else: 

174 raise IndexError 

175 

176 def get_height_for_line( 

177 self, 

178 lineno: int, 

179 width: int, 

180 get_line_prefix: GetLinePrefixCallable | None, 

181 slice_stop: int | None = None, 

182 ) -> int: 

183 """ 

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

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

186 

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

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

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

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

191 when line wrapping. 

192 :returns: The computed height. 

193 """ 

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

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

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

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

198 

199 try: 

200 return self._line_heights_cache[key] 

201 except KeyError: 

202 if width == 0: 

203 height = 10**8 

204 else: 

205 # Calculate line width first. 

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

207 text_width = get_cwidth(line) 

208 

209 if get_line_prefix: 

210 # Add prefix width. 

211 text_width += fragment_list_width( 

212 to_formatted_text(get_line_prefix(lineno, 0)) 

213 ) 

214 

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

216 height = 1 

217 

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

219 # Keep adding new prefixes for every wrapped line. 

220 while text_width > width: 

221 height += 1 

222 text_width -= width 

223 

224 fragments2 = to_formatted_text( 

225 get_line_prefix(lineno, height - 1) 

226 ) 

227 prefix_width = get_cwidth(fragment_list_to_text(fragments2)) 

228 

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

230 height = 10**8 

231 break 

232 

233 text_width += prefix_width 

234 else: 

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

236 try: 

237 quotient, remainder = divmod(text_width, width) 

238 except ZeroDivisionError: 

239 height = 10**8 

240 else: 

241 if remainder: 

242 quotient += 1 # Like math.ceil. 

243 height = max(1, quotient) 

244 

245 # Cache and return 

246 self._line_heights_cache[key] = height 

247 return height 

248 

249 

250class FormattedTextControl(UIControl): 

251 """ 

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

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

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

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

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

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

258 

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

260 

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

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

263 the cursor position: 

264 

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

266 with the current cursor position. 

267 

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

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

270 this will specify the cursor position. 

271 

272 Mouse support: 

273 

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

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

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

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

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

279 containing Window to handle this event. 

280 

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

282 focusable. 

283 

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

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

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

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

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

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

290 a `Point` instance. 

291 """ 

292 

293 def __init__( 

294 self, 

295 text: AnyFormattedText = "", 

296 style: str = "", 

297 focusable: FilterOrBool = False, 

298 key_bindings: KeyBindingsBase | None = None, 

299 show_cursor: bool = True, 

300 modal: bool = False, 

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

302 ) -> None: 

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

304 self.style = style 

305 self.focusable = to_filter(focusable) 

306 

307 # Key bindings. 

308 self.key_bindings = key_bindings 

309 self.show_cursor = show_cursor 

310 self.modal = modal 

311 self.get_cursor_position = get_cursor_position 

312 

313 #: Cache for the content. 

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

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

316 maxsize=1 

317 ) 

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

319 

320 # Render info for the mouse support. 

321 self._fragments: StyleAndTextTuples | None = None 

322 

323 def reset(self) -> None: 

324 self._fragments = None 

325 

326 def is_focusable(self) -> bool: 

327 return self.focusable() 

328 

329 def __repr__(self) -> str: 

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

331 

332 def _get_formatted_text_cached(self) -> StyleAndTextTuples: 

333 """ 

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

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

336 we also need those for calculating the dimensions.) 

337 """ 

338 return self._fragment_cache.get( 

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

340 ) 

341 

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

343 """ 

344 Return the preferred width for this control. 

345 That is the width of the longest line. 

346 """ 

347 text = fragment_list_to_text(self._get_formatted_text_cached()) 

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

349 return max(line_lengths) 

350 

351 def preferred_height( 

352 self, 

353 width: int, 

354 max_available_height: int, 

355 wrap_lines: bool, 

356 get_line_prefix: GetLinePrefixCallable | None, 

357 ) -> int | None: 

358 """ 

359 Return the preferred height for this control. 

360 """ 

361 content = self.create_content(width, None) 

362 if wrap_lines: 

363 height = 0 

364 for i in range(content.line_count): 

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

366 if height >= max_available_height: 

367 return max_available_height 

368 return height 

369 else: 

370 return content.line_count 

371 

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

373 # Get fragments 

374 fragments_with_mouse_handlers = self._get_formatted_text_cached() 

375 fragment_lines_with_mouse_handlers = list( 

376 split_lines(fragments_with_mouse_handlers) 

377 ) 

378 

379 # Strip mouse handlers from fragments. 

380 fragment_lines: list[StyleAndTextTuples] = [ 

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

382 for line in fragment_lines_with_mouse_handlers 

383 ] 

384 

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

386 # `mouse_handler`. 

387 self._fragments = fragments_with_mouse_handlers 

388 

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

390 # cursor position here. 

391 def get_cursor_position( 

392 fragment: str = "[SetCursorPosition]", 

393 ) -> Point | None: 

394 for y, line in enumerate(fragment_lines): 

395 x = 0 

396 for style_str, text, *_ in line: 

397 if fragment in style_str: 

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

399 x += len(text) 

400 return None 

401 

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

403 def get_menu_position() -> Point | None: 

404 return get_cursor_position("[SetMenuPosition]") 

405 

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

407 

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

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

410 

411 def get_content() -> UIContent: 

412 return UIContent( 

413 get_line=lambda i: fragment_lines[i], 

414 line_count=len(fragment_lines), 

415 show_cursor=self.show_cursor, 

416 cursor_position=cursor_position, 

417 menu_position=get_menu_position(), 

418 ) 

419 

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

421 

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

423 """ 

424 Handle mouse events. 

425 

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

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

428 return `NotImplemented` in case we want the 

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

430 event.) 

431 """ 

432 if self._fragments: 

433 # Read the generator. 

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

435 

436 try: 

437 fragments = fragments_for_line[mouse_event.position.y] 

438 except IndexError: 

439 return NotImplemented 

440 else: 

441 # Find position in the fragment list. 

442 xpos = mouse_event.position.x 

443 

444 # Find mouse handler for this character. 

445 count = 0 

446 for item in fragments: 

447 count += len(item[1]) 

448 if count > xpos: 

449 if len(item) >= 3: 

450 # Handler found. Call it. 

451 # (Handler can return NotImplemented, so return 

452 # that result.) 

453 handler = item[2] 

454 return handler(mouse_event) 

455 else: 

456 break 

457 

458 # Otherwise, don't handle here. 

459 return NotImplemented 

460 

461 def is_modal(self) -> bool: 

462 return self.modal 

463 

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

465 return self.key_bindings 

466 

467 

468class DummyControl(UIControl): 

469 """ 

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

471 

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

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

474 define the filling.) 

475 """ 

476 

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

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

479 return [] 

480 

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

482 

483 def is_focusable(self) -> bool: 

484 return False 

485 

486 

487class _ProcessedLine(NamedTuple): 

488 fragments: StyleAndTextTuples 

489 source_to_display: Callable[[int], int] 

490 display_to_source: Callable[[int], int] 

491 

492 

493class BufferControl(UIControl): 

494 """ 

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

496 

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

498 :param input_processors: A list of 

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

500 :param include_default_input_processors: When True, include the default 

501 processors for highlighting of selection, search and displaying of 

502 multiple cursors. 

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

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

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

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

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

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

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

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

511 """ 

512 

513 def __init__( 

514 self, 

515 buffer: Buffer | None = None, 

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

517 include_default_input_processors: bool = True, 

518 lexer: Lexer | None = None, 

519 preview_search: FilterOrBool = False, 

520 focusable: FilterOrBool = True, 

521 search_buffer_control: ( 

522 None | SearchBufferControl | Callable[[], SearchBufferControl] 

523 ) = None, 

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

525 focus_on_click: FilterOrBool = False, 

526 key_bindings: KeyBindingsBase | None = None, 

527 ): 

528 self.input_processors = input_processors 

529 self.include_default_input_processors = include_default_input_processors 

530 

531 self.default_input_processors = [ 

532 HighlightSearchProcessor(), 

533 HighlightIncrementalSearchProcessor(), 

534 HighlightSelectionProcessor(), 

535 DisplayMultipleCursors(), 

536 ] 

537 

538 self.preview_search = to_filter(preview_search) 

539 self.focusable = to_filter(focusable) 

540 self.focus_on_click = to_filter(focus_on_click) 

541 

542 self.buffer = buffer or Buffer() 

543 self.menu_position = menu_position 

544 self.lexer = lexer or SimpleLexer() 

545 self.key_bindings = key_bindings 

546 self._search_buffer_control = search_buffer_control 

547 

548 #: Cache for the lexer. 

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

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

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

552 self._fragment_cache: SimpleCache[ 

553 Hashable, Callable[[int], StyleAndTextTuples] 

554 ] = SimpleCache(maxsize=8) 

555 

556 self._last_click_timestamp: float | None = None 

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

558 

559 def __repr__(self) -> str: 

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

561 

562 @property 

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

564 result: SearchBufferControl | None 

565 

566 if callable(self._search_buffer_control): 

567 result = self._search_buffer_control() 

568 else: 

569 result = self._search_buffer_control 

570 

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

572 return result 

573 

574 @property 

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

576 control = self.search_buffer_control 

577 if control is not None: 

578 return control.buffer 

579 return None 

580 

581 @property 

582 def search_state(self) -> SearchState: 

583 """ 

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

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

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

587 `SearchState`. 

588 """ 

589 search_buffer_control = self.search_buffer_control 

590 if search_buffer_control: 

591 return search_buffer_control.searcher_search_state 

592 else: 

593 return SearchState() 

594 

595 def is_focusable(self) -> bool: 

596 return self.focusable() 

597 

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

599 """ 

600 This should return the preferred width. 

601 

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

603 because it would be too expensive. Calculating the preferred 

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

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

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

607 documents only would result in inconsistent behavior. 

608 """ 

609 return None 

610 

611 def preferred_height( 

612 self, 

613 width: int, 

614 max_available_height: int, 

615 wrap_lines: bool, 

616 get_line_prefix: GetLinePrefixCallable | None, 

617 ) -> int | None: 

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

619 # given width. 

620 height = 0 

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

622 

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

624 # of lines. 

625 if not wrap_lines: 

626 return content.line_count 

627 

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

629 # return max_available_height. No need to calculate anything. 

630 if content.line_count >= max_available_height: 

631 return max_available_height 

632 

633 for i in range(content.line_count): 

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

635 

636 if height >= max_available_height: 

637 return max_available_height 

638 

639 return height 

640 

641 def _get_formatted_text_for_line_func( 

642 self, document: Document 

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

644 """ 

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

646 """ 

647 

648 # Cache using `document.text`. 

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

650 return self.lexer.lex_document(document) 

651 

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

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

654 

655 def _create_get_processed_line_func( 

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

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

658 """ 

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

660 returns a _ProcessedLine(processed_fragments, source_to_display, display_to_source) 

661 tuple. 

662 """ 

663 # Merge all input processors together. 

664 input_processors = self.input_processors or [] 

665 if self.include_default_input_processors: 

666 input_processors = self.default_input_processors + input_processors 

667 

668 merged_processor = merge_processors(input_processors) 

669 

670 def transform( 

671 lineno: int, 

672 fragments: StyleAndTextTuples, 

673 get_line: Callable[[int], StyleAndTextTuples], 

674 ) -> _ProcessedLine: 

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

676 

677 # Get cursor position at this line. 

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

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

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

681 operation.""" 

682 return i 

683 

684 transformation = merged_processor.apply_transformation( 

685 TransformationInput( 

686 self, 

687 document, 

688 lineno, 

689 source_to_display, 

690 fragments, 

691 width, 

692 height, 

693 get_line, 

694 ) 

695 ) 

696 

697 return _ProcessedLine( 

698 transformation.fragments, 

699 transformation.source_to_display, 

700 transformation.display_to_source, 

701 ) 

702 

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

704 get_line = self._get_formatted_text_for_line_func(document) 

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

706 

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

708 try: 

709 return cache[i] 

710 except KeyError: 

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

712 cache[i] = processed_line 

713 return processed_line 

714 

715 return get_processed_line 

716 

717 return create_func() 

718 

719 def create_content( 

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

721 ) -> UIContent: 

722 """ 

723 Create a UIContent. 

724 """ 

725 buffer = self.buffer 

726 

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

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

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

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

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

732 # interaction (like in a key binding). 

733 buffer.load_history_if_not_yet_loaded() 

734 

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

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

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

738 # text/cursor position.) 

739 search_control = self.search_buffer_control 

740 preview_now = preview_search or bool( 

741 # Only if this feature is enabled. 

742 self.preview_search() 

743 and 

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

745 search_control 

746 and search_control.buffer.text 

747 and 

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

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

750 get_app().layout.search_target_buffer_control == self 

751 ) 

752 

753 if preview_now and search_control is not None: 

754 ss = self.search_state 

755 

756 document = buffer.document_for_search( 

757 SearchState( 

758 text=search_control.buffer.text, 

759 direction=ss.direction, 

760 ignore_case=ss.ignore_case, 

761 ) 

762 ) 

763 else: 

764 document = buffer.document 

765 

766 get_processed_line = self._create_get_processed_line_func( 

767 document, width, height 

768 ) 

769 self._last_get_processed_line = get_processed_line 

770 

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

772 "Return the content column for this coordinate." 

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

774 

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

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

777 fragments = get_processed_line(i).fragments 

778 

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

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

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

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

783 # cursor around.) 

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

785 return fragments 

786 

787 content = UIContent( 

788 get_line=get_line, 

789 line_count=document.line_count, 

790 cursor_position=translate_rowcol( 

791 document.cursor_position_row, document.cursor_position_col 

792 ), 

793 ) 

794 

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

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

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

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

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

800 if menu_position is not None: 

801 assert isinstance(menu_position, int) 

802 menu_row, menu_col = buffer.document.translate_index_to_position( 

803 menu_position 

804 ) 

805 content.menu_position = translate_rowcol(menu_row, menu_col) 

806 elif buffer.complete_state: 

807 # Position for completion menu. 

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

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

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

811 # can change and shorten the input.) 

812 menu_row, menu_col = buffer.document.translate_index_to_position( 

813 min( 

814 buffer.cursor_position, 

815 buffer.complete_state.original_document.cursor_position, 

816 ) 

817 ) 

818 content.menu_position = translate_rowcol(menu_row, menu_col) 

819 else: 

820 content.menu_position = None 

821 

822 return content 

823 

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

825 """ 

826 Mouse handler for this control. 

827 """ 

828 buffer = self.buffer 

829 position = mouse_event.position 

830 

831 # Focus buffer when clicked. 

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

833 if self._last_get_processed_line: 

834 processed_line = self._last_get_processed_line(position.y) 

835 

836 # Translate coordinates back to the cursor position of the 

837 # original input. 

838 xpos = processed_line.display_to_source(position.x) 

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

840 

841 # Set the cursor position. 

842 if mouse_event.event_type == MouseEventType.MOUSE_DOWN: 

843 buffer.exit_selection() 

844 buffer.cursor_position = index 

845 

846 elif ( 

847 mouse_event.event_type == MouseEventType.MOUSE_MOVE 

848 and mouse_event.button != MouseButton.NONE 

849 ): 

850 # Click and drag to highlight a selection 

851 if ( 

852 buffer.selection_state is None 

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

854 ): 

855 buffer.start_selection(selection_type=SelectionType.CHARACTERS) 

856 buffer.cursor_position = index 

857 

858 elif mouse_event.event_type == MouseEventType.MOUSE_UP: 

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

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

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

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

863 # will be repositioned automatically.) 

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

865 if buffer.selection_state is None: 

866 buffer.start_selection( 

867 selection_type=SelectionType.CHARACTERS 

868 ) 

869 buffer.cursor_position = index 

870 

871 # Select word around cursor on double click. 

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

873 double_click = ( 

874 self._last_click_timestamp 

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

876 ) 

877 self._last_click_timestamp = time.time() 

878 

879 if double_click: 

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

881 buffer.cursor_position += start 

882 buffer.start_selection(selection_type=SelectionType.CHARACTERS) 

883 buffer.cursor_position += end - start 

884 else: 

885 # Don't handle scroll events here. 

886 return NotImplemented 

887 

888 # Not focused, but focusing on click events. 

889 else: 

890 if ( 

891 self.focus_on_click() 

892 and mouse_event.event_type == MouseEventType.MOUSE_UP 

893 ): 

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

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

896 # focused and be handled anyway.) 

897 get_app().layout.current_control = self 

898 else: 

899 return NotImplemented 

900 

901 return None 

902 

903 def move_cursor_down(self) -> None: 

904 b = self.buffer 

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

906 

907 def move_cursor_up(self) -> None: 

908 b = self.buffer 

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

910 

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

912 """ 

913 When additional key bindings are given. Return these. 

914 """ 

915 return self.key_bindings 

916 

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

918 """ 

919 Return the Window invalidate events. 

920 """ 

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

922 yield self.buffer.on_text_changed 

923 yield self.buffer.on_cursor_position_changed 

924 

925 yield self.buffer.on_completions_changed 

926 yield self.buffer.on_suggestion_set 

927 

928 

929class SearchBufferControl(BufferControl): 

930 """ 

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

932 :class:`.BufferControl`. 

933 

934 :param ignore_case: Search case insensitive. 

935 """ 

936 

937 def __init__( 

938 self, 

939 buffer: Buffer | None = None, 

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

941 lexer: Lexer | None = None, 

942 focus_on_click: FilterOrBool = False, 

943 key_bindings: KeyBindingsBase | None = None, 

944 ignore_case: FilterOrBool = False, 

945 ): 

946 super().__init__( 

947 buffer=buffer, 

948 input_processors=input_processors, 

949 lexer=lexer, 

950 focus_on_click=focus_on_click, 

951 key_bindings=key_bindings, 

952 ) 

953 

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

955 # BufferControls, then represents the search state. 

956 self.searcher_search_state = SearchState(ignore_case=ignore_case)