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

327 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-25 06:05 +0000

1""" 

2User interface Controls for the layout. 

3""" 

4from __future__ import annotations 

5 

6import time 

7from abc import ABCMeta, abstractmethod 

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

9 

10from prompt_toolkit.application.current import get_app 

11from prompt_toolkit.buffer import Buffer 

12from prompt_toolkit.cache import SimpleCache 

13from prompt_toolkit.data_structures import Point 

14from prompt_toolkit.document import Document 

15from prompt_toolkit.filters import FilterOrBool, to_filter 

16from prompt_toolkit.formatted_text import ( 

17 AnyFormattedText, 

18 StyleAndTextTuples, 

19 to_formatted_text, 

20) 

21from prompt_toolkit.formatted_text.utils import ( 

22 fragment_list_to_text, 

23 fragment_list_width, 

24 split_lines, 

25) 

26from prompt_toolkit.lexers import Lexer, SimpleLexer 

27from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType 

28from prompt_toolkit.search import SearchState 

29from prompt_toolkit.selection import SelectionType 

30from prompt_toolkit.utils import get_cwidth 

31 

32from .processors import ( 

33 DisplayMultipleCursors, 

34 HighlightIncrementalSearchProcessor, 

35 HighlightSearchProcessor, 

36 HighlightSelectionProcessor, 

37 Processor, 

38 TransformationInput, 

39 merge_processors, 

40) 

41 

42if TYPE_CHECKING: 

43 from prompt_toolkit.key_binding.key_bindings import ( 

44 KeyBindingsBase, 

45 NotImplementedOrNone, 

46 ) 

47 from prompt_toolkit.utils import Event 

48 

49 

50__all__ = [ 

51 "BufferControl", 

52 "SearchBufferControl", 

53 "DummyControl", 

54 "FormattedTextControl", 

55 "UIControl", 

56 "UIContent", 

57] 

58 

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

60 

61 

62class UIControl(metaclass=ABCMeta): 

63 """ 

64 Base class for all user interface controls. 

65 """ 

66 

67 def reset(self) -> None: 

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

69 pass 

70 

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

72 return None 

73 

74 def preferred_height( 

75 self, 

76 width: int, 

77 max_available_height: int, 

78 wrap_lines: bool, 

79 get_line_prefix: GetLinePrefixCallable | None, 

80 ) -> int | None: 

81 return None 

82 

83 def is_focusable(self) -> bool: 

84 """ 

85 Tell whether this user control is focusable. 

86 """ 

87 return False 

88 

89 @abstractmethod 

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

91 """ 

92 Generate the content for this user control. 

93 

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

95 """ 

96 

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

98 """ 

99 Handle mouse events. 

100 

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

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

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

104 

105 :param mouse_event: `MouseEvent` instance. 

106 """ 

107 return NotImplemented 

108 

109 def move_cursor_down(self) -> None: 

110 """ 

111 Request to move the cursor down. 

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

113 top. 

114 """ 

115 

116 def move_cursor_up(self) -> None: 

117 """ 

118 Request to move the cursor up. 

119 """ 

120 

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

122 """ 

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

124 

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

126 specified, or `None` otherwise. 

127 """ 

128 

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

130 """ 

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

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

133 handlers to these events.) 

134 """ 

135 return [] 

136 

137 

138class UIContent: 

139 """ 

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

141 lines. 

142 

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

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

145 :param line_count: The number of lines. 

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

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

148 :param show_cursor: Make the cursor visible. 

149 """ 

150 

151 def __init__( 

152 self, 

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

154 line_count: int = 0, 

155 cursor_position: Point | None = None, 

156 menu_position: Point | None = None, 

157 show_cursor: bool = True, 

158 ): 

159 self.get_line = get_line 

160 self.line_count = line_count 

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

162 self.menu_position = menu_position 

163 self.show_cursor = show_cursor 

164 

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

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

167 

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

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

170 if lineno < self.line_count: 

171 return self.get_line(lineno) 

172 else: 

173 raise IndexError 

174 

175 def get_height_for_line( 

176 self, 

177 lineno: int, 

178 width: int, 

179 get_line_prefix: GetLinePrefixCallable | None, 

180 slice_stop: int | None = None, 

181 ) -> int: 

182 """ 

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

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

185 

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

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

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

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

190 when line wrapping. 

191 :returns: The computed height. 

192 """ 

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

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

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

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

197 

198 try: 

199 return self._line_heights_cache[key] 

200 except KeyError: 

201 if width == 0: 

202 height = 10**8 

203 else: 

204 # Calculate line width first. 

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

206 text_width = get_cwidth(line) 

207 

208 if get_line_prefix: 

209 # Add prefix width. 

210 text_width += fragment_list_width( 

211 to_formatted_text(get_line_prefix(lineno, 0)) 

212 ) 

213 

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

215 height = 1 

216 

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

218 # Keep adding new prefixes for every wrapped line. 

219 while text_width > width: 

220 height += 1 

221 text_width -= width 

222 

223 fragments2 = to_formatted_text( 

224 get_line_prefix(lineno, height - 1) 

225 ) 

226 prefix_width = get_cwidth(fragment_list_to_text(fragments2)) 

227 

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

229 height = 10**8 

230 break 

231 

232 text_width += prefix_width 

233 else: 

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

235 try: 

236 quotient, remainder = divmod(text_width, width) 

237 except ZeroDivisionError: 

238 height = 10**8 

239 else: 

240 if remainder: 

241 quotient += 1 # Like math.ceil. 

242 height = max(1, quotient) 

243 

244 # Cache and return 

245 self._line_heights_cache[key] = height 

246 return height 

247 

248 

249class FormattedTextControl(UIControl): 

250 """ 

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

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

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

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

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

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

257 

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

259 

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

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

262 the cursor position: 

263 

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

265 with the current cursor position. 

266 

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

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

269 this will specify the cursor position. 

270 

271 Mouse support: 

272 

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

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

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

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

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

278 containing Window to handle this event. 

279 

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

281 focusable. 

282 

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

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

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

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

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

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

289 a `Point` instance. 

290 """ 

291 

292 def __init__( 

293 self, 

294 text: AnyFormattedText = "", 

295 style: str = "", 

296 focusable: FilterOrBool = False, 

297 key_bindings: KeyBindingsBase | None = None, 

298 show_cursor: bool = True, 

299 modal: bool = False, 

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

301 ) -> None: 

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

303 self.style = style 

304 self.focusable = to_filter(focusable) 

305 

306 # Key bindings. 

307 self.key_bindings = key_bindings 

308 self.show_cursor = show_cursor 

309 self.modal = modal 

310 self.get_cursor_position = get_cursor_position 

311 

312 #: Cache for the content. 

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

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

315 maxsize=1 

316 ) 

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

318 

319 # Render info for the mouse support. 

320 self._fragments: StyleAndTextTuples | None = None 

321 

322 def reset(self) -> None: 

323 self._fragments = None 

324 

325 def is_focusable(self) -> bool: 

326 return self.focusable() 

327 

328 def __repr__(self) -> str: 

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

330 

331 def _get_formatted_text_cached(self) -> StyleAndTextTuples: 

332 """ 

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

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

335 we also need those for calculating the dimensions.) 

336 """ 

337 return self._fragment_cache.get( 

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

339 ) 

340 

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

342 """ 

343 Return the preferred width for this control. 

344 That is the width of the longest line. 

345 """ 

346 text = fragment_list_to_text(self._get_formatted_text_cached()) 

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

348 return max(line_lengths) 

349 

350 def preferred_height( 

351 self, 

352 width: int, 

353 max_available_height: int, 

354 wrap_lines: bool, 

355 get_line_prefix: GetLinePrefixCallable | None, 

356 ) -> int | None: 

357 """ 

358 Return the preferred height for this control. 

359 """ 

360 content = self.create_content(width, None) 

361 if wrap_lines: 

362 height = 0 

363 for i in range(content.line_count): 

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

365 if height >= max_available_height: 

366 return max_available_height 

367 return height 

368 else: 

369 return content.line_count 

370 

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

372 # Get fragments 

373 fragments_with_mouse_handlers = self._get_formatted_text_cached() 

374 fragment_lines_with_mouse_handlers = list( 

375 split_lines(fragments_with_mouse_handlers) 

376 ) 

377 

378 # Strip mouse handlers from fragments. 

379 fragment_lines: list[StyleAndTextTuples] = [ 

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

381 for line in fragment_lines_with_mouse_handlers 

382 ] 

383 

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

385 # `mouse_handler`. 

386 self._fragments = fragments_with_mouse_handlers 

387 

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

389 # cursor position here. 

390 def get_cursor_position( 

391 fragment: str = "[SetCursorPosition]", 

392 ) -> Point | None: 

393 for y, line in enumerate(fragment_lines): 

394 x = 0 

395 for style_str, text, *_ in line: 

396 if fragment in style_str: 

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

398 x += len(text) 

399 return None 

400 

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

402 def get_menu_position() -> Point | None: 

403 return get_cursor_position("[SetMenuPosition]") 

404 

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

406 

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

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

409 

410 def get_content() -> UIContent: 

411 return UIContent( 

412 get_line=lambda i: fragment_lines[i], 

413 line_count=len(fragment_lines), 

414 show_cursor=self.show_cursor, 

415 cursor_position=cursor_position, 

416 menu_position=get_menu_position(), 

417 ) 

418 

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

420 

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

422 """ 

423 Handle mouse events. 

424 

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

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

427 return `NotImplemented` in case we want the 

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

429 event.) 

430 """ 

431 if self._fragments: 

432 # Read the generator. 

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

434 

435 try: 

436 fragments = fragments_for_line[mouse_event.position.y] 

437 except IndexError: 

438 return NotImplemented 

439 else: 

440 # Find position in the fragment list. 

441 xpos = mouse_event.position.x 

442 

443 # Find mouse handler for this character. 

444 count = 0 

445 for item in fragments: 

446 count += len(item[1]) 

447 if count > xpos: 

448 if len(item) >= 3: 

449 # Handler found. Call it. 

450 # (Handler can return NotImplemented, so return 

451 # that result.) 

452 handler = item[2] # type: ignore 

453 return handler(mouse_event) 

454 else: 

455 break 

456 

457 # Otherwise, don't handle here. 

458 return NotImplemented 

459 

460 def is_modal(self) -> bool: 

461 return self.modal 

462 

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

464 return self.key_bindings 

465 

466 

467class DummyControl(UIControl): 

468 """ 

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

470 

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

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

473 define the filling.) 

474 """ 

475 

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

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

478 return [] 

479 

480 return UIContent( 

481 get_line=get_line, line_count=100**100 

482 ) # 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 visualising 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 behaviour. 

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(lineno: int, fragments: StyleAndTextTuples) -> _ProcessedLine: 

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

673 

674 # Get cursor position at this line. 

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

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

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

678 operation.""" 

679 return i 

680 

681 transformation = merged_processor.apply_transformation( 

682 TransformationInput( 

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

684 ) 

685 ) 

686 

687 return _ProcessedLine( 

688 transformation.fragments, 

689 transformation.source_to_display, 

690 transformation.display_to_source, 

691 ) 

692 

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

694 get_line = self._get_formatted_text_for_line_func(document) 

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

696 

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

698 try: 

699 return cache[i] 

700 except KeyError: 

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

702 cache[i] = processed_line 

703 return processed_line 

704 

705 return get_processed_line 

706 

707 return create_func() 

708 

709 def create_content( 

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

711 ) -> UIContent: 

712 """ 

713 Create a UIContent. 

714 """ 

715 buffer = self.buffer 

716 

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

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

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

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

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

722 # interaction (like in a key binding). 

723 buffer.load_history_if_not_yet_loaded() 

724 

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

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

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

728 # text/cursor position.) 

729 search_control = self.search_buffer_control 

730 preview_now = preview_search or bool( 

731 # Only if this feature is enabled. 

732 self.preview_search() 

733 and 

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

735 search_control 

736 and search_control.buffer.text 

737 and 

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

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

740 get_app().layout.search_target_buffer_control == self 

741 ) 

742 

743 if preview_now and search_control is not None: 

744 ss = self.search_state 

745 

746 document = buffer.document_for_search( 

747 SearchState( 

748 text=search_control.buffer.text, 

749 direction=ss.direction, 

750 ignore_case=ss.ignore_case, 

751 ) 

752 ) 

753 else: 

754 document = buffer.document 

755 

756 get_processed_line = self._create_get_processed_line_func( 

757 document, width, height 

758 ) 

759 self._last_get_processed_line = get_processed_line 

760 

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

762 "Return the content column for this coordinate." 

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

764 

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

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

767 fragments = get_processed_line(i).fragments 

768 

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

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

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

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

773 # cursor around.) 

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

775 return fragments 

776 

777 content = UIContent( 

778 get_line=get_line, 

779 line_count=document.line_count, 

780 cursor_position=translate_rowcol( 

781 document.cursor_position_row, document.cursor_position_col 

782 ), 

783 ) 

784 

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

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

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

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

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

790 if menu_position is not None: 

791 assert isinstance(menu_position, int) 

792 menu_row, menu_col = buffer.document.translate_index_to_position( 

793 menu_position 

794 ) 

795 content.menu_position = translate_rowcol(menu_row, menu_col) 

796 elif buffer.complete_state: 

797 # Position for completion menu. 

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

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

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

801 # can change and shorten the input.) 

802 menu_row, menu_col = buffer.document.translate_index_to_position( 

803 min( 

804 buffer.cursor_position, 

805 buffer.complete_state.original_document.cursor_position, 

806 ) 

807 ) 

808 content.menu_position = translate_rowcol(menu_row, menu_col) 

809 else: 

810 content.menu_position = None 

811 

812 return content 

813 

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

815 """ 

816 Mouse handler for this control. 

817 """ 

818 buffer = self.buffer 

819 position = mouse_event.position 

820 

821 # Focus buffer when clicked. 

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

823 if self._last_get_processed_line: 

824 processed_line = self._last_get_processed_line(position.y) 

825 

826 # Translate coordinates back to the cursor position of the 

827 # original input. 

828 xpos = processed_line.display_to_source(position.x) 

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

830 

831 # Set the cursor position. 

832 if mouse_event.event_type == MouseEventType.MOUSE_DOWN: 

833 buffer.exit_selection() 

834 buffer.cursor_position = index 

835 

836 elif ( 

837 mouse_event.event_type == MouseEventType.MOUSE_MOVE 

838 and mouse_event.button != MouseButton.NONE 

839 ): 

840 # Click and drag to highlight a selection 

841 if ( 

842 buffer.selection_state is None 

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

844 ): 

845 buffer.start_selection(selection_type=SelectionType.CHARACTERS) 

846 buffer.cursor_position = index 

847 

848 elif mouse_event.event_type == MouseEventType.MOUSE_UP: 

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

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

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

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

853 # will be repositioned automatically.) 

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

855 if buffer.selection_state is None: 

856 buffer.start_selection( 

857 selection_type=SelectionType.CHARACTERS 

858 ) 

859 buffer.cursor_position = index 

860 

861 # Select word around cursor on double click. 

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

863 double_click = ( 

864 self._last_click_timestamp 

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

866 ) 

867 self._last_click_timestamp = time.time() 

868 

869 if double_click: 

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

871 buffer.cursor_position += start 

872 buffer.start_selection(selection_type=SelectionType.CHARACTERS) 

873 buffer.cursor_position += end - start 

874 else: 

875 # Don't handle scroll events here. 

876 return NotImplemented 

877 

878 # Not focused, but focusing on click events. 

879 else: 

880 if ( 

881 self.focus_on_click() 

882 and mouse_event.event_type == MouseEventType.MOUSE_UP 

883 ): 

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

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

886 # focused and be handled anyway.) 

887 get_app().layout.current_control = self 

888 else: 

889 return NotImplemented 

890 

891 return None 

892 

893 def move_cursor_down(self) -> None: 

894 b = self.buffer 

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

896 

897 def move_cursor_up(self) -> None: 

898 b = self.buffer 

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

900 

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

902 """ 

903 When additional key bindings are given. Return these. 

904 """ 

905 return self.key_bindings 

906 

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

908 """ 

909 Return the Window invalidate events. 

910 """ 

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

912 yield self.buffer.on_text_changed 

913 yield self.buffer.on_cursor_position_changed 

914 

915 yield self.buffer.on_completions_changed 

916 yield self.buffer.on_suggestion_set 

917 

918 

919class SearchBufferControl(BufferControl): 

920 """ 

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

922 :class:`.BufferControl`. 

923 

924 :param ignore_case: Search case insensitive. 

925 """ 

926 

927 def __init__( 

928 self, 

929 buffer: Buffer | None = None, 

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

931 lexer: Lexer | None = None, 

932 focus_on_click: FilterOrBool = False, 

933 key_bindings: KeyBindingsBase | None = None, 

934 ignore_case: FilterOrBool = False, 

935 ): 

936 super().__init__( 

937 buffer=buffer, 

938 input_processors=input_processors, 

939 lexer=lexer, 

940 focus_on_click=focus_on_click, 

941 key_bindings=key_bindings, 

942 ) 

943 

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

945 # BufferControls, then represents the search state. 

946 self.searcher_search_state = SearchState(ignore_case=ignore_case)