Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/prompt_toolkit/key_binding/bindings/vi.py: 6%

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

961 statements  

1# pylint: disable=function-redefined 

2from __future__ import annotations 

3 

4import codecs 

5import string 

6from collections.abc import Callable, Iterable 

7from enum import Enum 

8from itertools import accumulate 

9from typing import TypeVar 

10 

11from prompt_toolkit.application.current import get_app 

12from prompt_toolkit.buffer import Buffer, indent, reshape_text, unindent 

13from prompt_toolkit.clipboard import ClipboardData 

14from prompt_toolkit.document import Document 

15from prompt_toolkit.filters import ( 

16 Always, 

17 Condition, 

18 Filter, 

19 has_arg, 

20 is_read_only, 

21 is_searching, 

22) 

23from prompt_toolkit.filters.app import ( 

24 in_paste_mode, 

25 is_multiline, 

26 vi_digraph_mode, 

27 vi_insert_mode, 

28 vi_insert_multiple_mode, 

29 vi_mode, 

30 vi_navigation_mode, 

31 vi_recording_macro, 

32 vi_replace_mode, 

33 vi_replace_single_mode, 

34 vi_search_direction_reversed, 

35 vi_selection_mode, 

36 vi_waiting_for_text_object_mode, 

37) 

38from prompt_toolkit.input.vt100_parser import Vt100Parser 

39from prompt_toolkit.key_binding.digraphs import DIGRAPHS 

40from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent 

41from prompt_toolkit.key_binding.vi_state import CharacterFind, InputMode 

42from prompt_toolkit.keys import Keys 

43from prompt_toolkit.search import SearchDirection 

44from prompt_toolkit.selection import PasteMode, SelectionState, SelectionType 

45 

46from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase 

47from .named_commands import get_by_name 

48 

49__all__ = [ 

50 "load_vi_bindings", 

51 "load_vi_search_bindings", 

52] 

53 

54E = KeyPressEvent 

55 

56ascii_lowercase = string.ascii_lowercase 

57 

58vi_register_names = ascii_lowercase + "0123456789" 

59 

60 

61class TextObjectType(Enum): 

62 EXCLUSIVE = "EXCLUSIVE" 

63 INCLUSIVE = "INCLUSIVE" 

64 LINEWISE = "LINEWISE" 

65 BLOCK = "BLOCK" 

66 

67 

68class TextObject: 

69 """ 

70 Return struct for functions wrapped in ``text_object``. 

71 Both `start` and `end` are relative to the current cursor position. 

72 """ 

73 

74 def __init__( 

75 self, start: int, end: int = 0, type: TextObjectType = TextObjectType.EXCLUSIVE 

76 ): 

77 self.start = start 

78 self.end = end 

79 self.type = type 

80 

81 @property 

82 def selection_type(self) -> SelectionType: 

83 if self.type == TextObjectType.LINEWISE: 

84 return SelectionType.LINES 

85 if self.type == TextObjectType.BLOCK: 

86 return SelectionType.BLOCK 

87 else: 

88 return SelectionType.CHARACTERS 

89 

90 def sorted(self) -> tuple[int, int]: 

91 """ 

92 Return a (start, end) tuple where start <= end. 

93 """ 

94 if self.start < self.end: 

95 return self.start, self.end 

96 else: 

97 return self.end, self.start 

98 

99 def operator_range(self, document: Document) -> tuple[int, int]: 

100 """ 

101 Return a (start, end) tuple with start <= end that indicates the range 

102 operators should operate on. 

103 `buffer` is used to get start and end of line positions. 

104 

105 This should return something that can be used in a slice, so the `end` 

106 position is *not* included. 

107 """ 

108 start, end = self.sorted() 

109 doc = document 

110 

111 if ( 

112 self.type == TextObjectType.EXCLUSIVE 

113 and doc.translate_index_to_position(end + doc.cursor_position)[1] == 0 

114 ): 

115 # If the motion is exclusive and the end of motion is on the first 

116 # column, the end position becomes end of previous line. 

117 end -= 1 

118 if self.type == TextObjectType.INCLUSIVE: 

119 end += 1 

120 if self.type == TextObjectType.LINEWISE: 

121 # Select whole lines 

122 row, col = doc.translate_index_to_position(start + doc.cursor_position) 

123 start = doc.translate_row_col_to_index(row, 0) - doc.cursor_position 

124 row, col = doc.translate_index_to_position(end + doc.cursor_position) 

125 end = ( 

126 doc.translate_row_col_to_index(row, len(doc.lines[row])) 

127 - doc.cursor_position 

128 ) 

129 return start, end 

130 

131 def get_line_numbers(self, buffer: Buffer) -> tuple[int, int]: 

132 """ 

133 Return a (start_line, end_line) pair. 

134 """ 

135 # Get absolute cursor positions from the text object. 

136 from_, to = self.operator_range(buffer.document) 

137 from_ += buffer.cursor_position 

138 to += buffer.cursor_position 

139 

140 # Take the start of the lines. 

141 from_, _ = buffer.document.translate_index_to_position(from_) 

142 to, _ = buffer.document.translate_index_to_position(to) 

143 

144 return from_, to 

145 

146 def cut(self, buffer: Buffer) -> tuple[Document, ClipboardData]: 

147 """ 

148 Turn text object into `ClipboardData` instance. 

149 """ 

150 from_, to = self.operator_range(buffer.document) 

151 

152 from_ += buffer.cursor_position 

153 to += buffer.cursor_position 

154 

155 # For Vi mode, the SelectionState does include the upper position, 

156 # while `self.operator_range` does not. So, go one to the left, unless 

157 # we're in the line mode, then we don't want to risk going to the 

158 # previous line, and missing one line in the selection. 

159 if self.type != TextObjectType.LINEWISE: 

160 to -= 1 

161 

162 document = Document( 

163 buffer.text, 

164 to, 

165 SelectionState(original_cursor_position=from_, type=self.selection_type), 

166 ) 

167 

168 new_document, clipboard_data = document.cut_selection() 

169 return new_document, clipboard_data 

170 

171 

172# Typevar for any text object function: 

173TextObjectFunction = Callable[[E], TextObject] 

174_TOF = TypeVar("_TOF", bound=TextObjectFunction) 

175 

176 

177def create_text_object_decorator( 

178 key_bindings: KeyBindings, 

179) -> Callable[..., Callable[[_TOF], _TOF]]: 

180 """ 

181 Create a decorator that can be used to register Vi text object implementations. 

182 """ 

183 

184 def text_object_decorator( 

185 *keys: Keys | str, 

186 filter: Filter = Always(), 

187 no_move_handler: bool = False, 

188 no_selection_handler: bool = False, 

189 eager: bool = False, 

190 ) -> Callable[[_TOF], _TOF]: 

191 """ 

192 Register a text object function. 

193 

194 Usage:: 

195 

196 @text_object('w', filter=..., no_move_handler=False) 

197 def handler(event): 

198 # Return a text object for this key. 

199 return TextObject(...) 

200 

201 :param no_move_handler: Disable the move handler in navigation mode. 

202 (It's still active in selection mode.) 

203 """ 

204 

205 def decorator(text_object_func: _TOF) -> _TOF: 

206 @key_bindings.add( 

207 *keys, filter=vi_waiting_for_text_object_mode & filter, eager=eager 

208 ) 

209 def _apply_operator_to_text_object(event: E) -> None: 

210 # Arguments are multiplied. 

211 vi_state = event.app.vi_state 

212 event._arg = str((vi_state.operator_arg or 1) * (event.arg or 1)) 

213 

214 # Call the text object handler. 

215 text_obj = text_object_func(event) 

216 

217 # Get the operator function. 

218 # (Should never be None here, given the 

219 # `vi_waiting_for_text_object_mode` filter state.) 

220 operator_func = vi_state.operator_func 

221 

222 if text_obj is not None and operator_func is not None: 

223 # Call the operator function with the text object. 

224 operator_func(event, text_obj) 

225 

226 # Clear operator. 

227 event.app.vi_state.operator_func = None 

228 event.app.vi_state.operator_arg = None 

229 

230 # Register a move operation. (Doesn't need an operator.) 

231 if not no_move_handler: 

232 

233 @key_bindings.add( 

234 *keys, 

235 filter=~vi_waiting_for_text_object_mode 

236 & filter 

237 & vi_navigation_mode, 

238 eager=eager, 

239 ) 

240 def _move_in_navigation_mode(event: E) -> None: 

241 """ 

242 Move handler for navigation mode. 

243 """ 

244 text_object = text_object_func(event) 

245 event.current_buffer.cursor_position += text_object.start 

246 

247 # Register a move selection operation. 

248 if not no_selection_handler: 

249 

250 @key_bindings.add( 

251 *keys, 

252 filter=~vi_waiting_for_text_object_mode 

253 & filter 

254 & vi_selection_mode, 

255 eager=eager, 

256 ) 

257 def _move_in_selection_mode(event: E) -> None: 

258 """ 

259 Move handler for selection mode. 

260 """ 

261 text_object = text_object_func(event) 

262 buff = event.current_buffer 

263 selection_state = buff.selection_state 

264 

265 if selection_state is None: 

266 return # Should not happen, because of the `vi_selection_mode` filter. 

267 

268 # When the text object has both a start and end position, like 'i(' or 'iw', 

269 # Turn this into a selection, otherwise the cursor. 

270 if text_object.end: 

271 # Take selection positions from text object. 

272 start, end = text_object.operator_range(buff.document) 

273 start += buff.cursor_position 

274 end += buff.cursor_position 

275 

276 selection_state.original_cursor_position = start 

277 buff.cursor_position = end 

278 

279 # Take selection type from text object. 

280 if text_object.type == TextObjectType.LINEWISE: 

281 selection_state.type = SelectionType.LINES 

282 else: 

283 selection_state.type = SelectionType.CHARACTERS 

284 else: 

285 event.current_buffer.cursor_position += text_object.start 

286 

287 # Make it possible to chain @text_object decorators. 

288 return text_object_func 

289 

290 return decorator 

291 

292 return text_object_decorator 

293 

294 

295# Typevar for any operator function: 

296OperatorFunction = Callable[[E, TextObject], None] 

297_OF = TypeVar("_OF", bound=OperatorFunction) 

298 

299 

300def create_operator_decorator( 

301 key_bindings: KeyBindings, 

302) -> Callable[..., Callable[[_OF], _OF]]: 

303 """ 

304 Create a decorator that can be used for registering Vi operators. 

305 """ 

306 

307 def operator_decorator( 

308 *keys: Keys | str, filter: Filter = Always(), eager: bool = False 

309 ) -> Callable[[_OF], _OF]: 

310 """ 

311 Register a Vi operator. 

312 

313 Usage:: 

314 

315 @operator('d', filter=...) 

316 def handler(event, text_object): 

317 # Do something with the text object here. 

318 """ 

319 

320 def decorator(operator_func: _OF) -> _OF: 

321 @key_bindings.add( 

322 *keys, 

323 filter=~vi_waiting_for_text_object_mode & filter & vi_navigation_mode, 

324 eager=eager, 

325 ) 

326 def _operator_in_navigation(event: E) -> None: 

327 """ 

328 Handle operator in navigation mode. 

329 """ 

330 # When this key binding is matched, only set the operator 

331 # function in the ViState. We should execute it after a text 

332 # object has been received. 

333 event.app.vi_state.operator_func = operator_func 

334 event.app.vi_state.operator_arg = event.arg 

335 

336 @key_bindings.add( 

337 *keys, 

338 filter=~vi_waiting_for_text_object_mode & filter & vi_selection_mode, 

339 eager=eager, 

340 ) 

341 def _operator_in_selection(event: E) -> None: 

342 """ 

343 Handle operator in selection mode. 

344 """ 

345 buff = event.current_buffer 

346 selection_state = buff.selection_state 

347 

348 if selection_state is not None: 

349 # Create text object from selection. 

350 if selection_state.type == SelectionType.LINES: 

351 text_obj_type = TextObjectType.LINEWISE 

352 elif selection_state.type == SelectionType.BLOCK: 

353 text_obj_type = TextObjectType.BLOCK 

354 else: 

355 text_obj_type = TextObjectType.INCLUSIVE 

356 

357 text_object = TextObject( 

358 selection_state.original_cursor_position - buff.cursor_position, 

359 type=text_obj_type, 

360 ) 

361 

362 # Execute operator. 

363 operator_func(event, text_object) 

364 

365 # Quit selection mode. 

366 buff.selection_state = None 

367 

368 return operator_func 

369 

370 return decorator 

371 

372 return operator_decorator 

373 

374 

375@Condition 

376def is_returnable() -> bool: 

377 return get_app().current_buffer.is_returnable 

378 

379 

380@Condition 

381def in_block_selection() -> bool: 

382 buff = get_app().current_buffer 

383 return bool( 

384 buff.selection_state and buff.selection_state.type == SelectionType.BLOCK 

385 ) 

386 

387 

388@Condition 

389def digraph_symbol_1_given() -> bool: 

390 return get_app().vi_state.digraph_symbol1 is not None 

391 

392 

393@Condition 

394def search_buffer_is_empty() -> bool: 

395 "Returns True when the search buffer is empty." 

396 return get_app().current_buffer.text == "" 

397 

398 

399@Condition 

400def tilde_operator() -> bool: 

401 return get_app().vi_state.tilde_operator 

402 

403 

404def load_vi_bindings() -> KeyBindingsBase: 

405 """ 

406 Vi extensions. 

407 

408 # Overview of Readline Vi commands: 

409 # http://www.catonmat.net/download/bash-vi-editing-mode-cheat-sheet.pdf 

410 """ 

411 # Note: Some key bindings have the "~IsReadOnly()" filter added. This 

412 # prevents the handler to be executed when the focus is on a 

413 # read-only buffer. 

414 # This is however only required for those that change the ViState to 

415 # INSERT mode. The `Buffer` class itself throws the 

416 # `EditReadOnlyBuffer` exception for any text operations which is 

417 # handled correctly. There is no need to add "~IsReadOnly" to all key 

418 # bindings that do text manipulation. 

419 

420 key_bindings = KeyBindings() 

421 handle = key_bindings.add 

422 

423 # (Note: Always take the navigation bindings in read-only mode, even when 

424 # ViState says different.) 

425 

426 TransformFunction = tuple[tuple[str, ...], Filter, Callable[[str], str]] 

427 

428 vi_transform_functions: list[TransformFunction] = [ 

429 # Rot 13 transformation 

430 ( 

431 ("g", "?"), 

432 Always(), 

433 lambda string: codecs.encode(string, "rot_13"), 

434 ), 

435 # To lowercase 

436 (("g", "u"), Always(), lambda string: string.lower()), 

437 # To uppercase. 

438 (("g", "U"), Always(), lambda string: string.upper()), 

439 # Swap case. 

440 (("g", "~"), Always(), lambda string: string.swapcase()), 

441 ( 

442 ("~",), 

443 tilde_operator, 

444 lambda string: string.swapcase(), 

445 ), 

446 ] 

447 

448 # Insert a character literally (quoted insert). 

449 handle("c-v", filter=vi_insert_mode)(get_by_name("quoted-insert")) 

450 

451 @handle("escape") 

452 def _back_to_navigation(event: E) -> None: 

453 """ 

454 Escape goes to vi navigation mode. 

455 """ 

456 buffer = event.current_buffer 

457 vi_state = event.app.vi_state 

458 

459 if vi_state.input_mode in (InputMode.INSERT, InputMode.REPLACE): 

460 buffer.cursor_position += buffer.document.get_cursor_left_position() 

461 

462 vi_state.input_mode = InputMode.NAVIGATION 

463 

464 if bool(buffer.selection_state): 

465 buffer.exit_selection() 

466 

467 @handle("k", filter=vi_selection_mode) 

468 def _up_in_selection(event: E) -> None: 

469 """ 

470 Arrow up in selection mode. 

471 """ 

472 event.current_buffer.cursor_up(count=event.arg) 

473 

474 @handle("j", filter=vi_selection_mode) 

475 def _down_in_selection(event: E) -> None: 

476 """ 

477 Arrow down in selection mode. 

478 """ 

479 event.current_buffer.cursor_down(count=event.arg) 

480 

481 @handle("up", filter=vi_navigation_mode) 

482 @handle("c-p", filter=vi_navigation_mode) 

483 def _up_in_navigation(event: E) -> None: 

484 """ 

485 Arrow up and ControlP in navigation mode go up. 

486 """ 

487 event.current_buffer.auto_up(count=event.arg) 

488 

489 @handle("k", filter=vi_navigation_mode) 

490 def _go_up(event: E) -> None: 

491 """ 

492 Go up, but if we enter a new history entry, move to the start of the 

493 line. 

494 """ 

495 event.current_buffer.auto_up( 

496 count=event.arg, go_to_start_of_line_if_history_changes=True 

497 ) 

498 

499 @handle("down", filter=vi_navigation_mode) 

500 @handle("c-n", filter=vi_navigation_mode) 

501 def _go_down(event: E) -> None: 

502 """ 

503 Arrow down and Control-N in navigation mode. 

504 """ 

505 event.current_buffer.auto_down(count=event.arg) 

506 

507 @handle("j", filter=vi_navigation_mode) 

508 def _go_down2(event: E) -> None: 

509 """ 

510 Go down, but if we enter a new history entry, go to the start of the line. 

511 """ 

512 event.current_buffer.auto_down( 

513 count=event.arg, go_to_start_of_line_if_history_changes=True 

514 ) 

515 

516 @handle("backspace", filter=vi_navigation_mode) 

517 def _go_left(event: E) -> None: 

518 """ 

519 In navigation-mode, move cursor. 

520 """ 

521 event.current_buffer.cursor_position += ( 

522 event.current_buffer.document.get_cursor_left_position(count=event.arg) 

523 ) 

524 

525 @handle("c-n", filter=vi_insert_mode) 

526 def _complete_next(event: E) -> None: 

527 b = event.current_buffer 

528 

529 if b.complete_state: 

530 b.complete_next() 

531 else: 

532 b.start_completion(select_first=True) 

533 

534 @handle("c-p", filter=vi_insert_mode) 

535 def _complete_prev(event: E) -> None: 

536 """ 

537 Control-P: To previous completion. 

538 """ 

539 b = event.current_buffer 

540 

541 if b.complete_state: 

542 b.complete_previous() 

543 else: 

544 b.start_completion(select_last=True) 

545 

546 @handle("c-g", filter=vi_insert_mode) 

547 @handle("c-y", filter=vi_insert_mode) 

548 def _accept_completion(event: E) -> None: 

549 """ 

550 Accept current completion. 

551 """ 

552 event.current_buffer.complete_state = None 

553 

554 @handle("c-e", filter=vi_insert_mode) 

555 def _cancel_completion(event: E) -> None: 

556 """ 

557 Cancel completion. Go back to originally typed text. 

558 """ 

559 event.current_buffer.cancel_completion() 

560 

561 # In navigation mode, pressing enter will always return the input. 

562 handle("enter", filter=vi_navigation_mode & is_returnable)( 

563 get_by_name("accept-line") 

564 ) 

565 

566 # In insert mode, also accept input when enter is pressed, and the buffer 

567 # has been marked as single line. 

568 handle("enter", filter=is_returnable & ~is_multiline)(get_by_name("accept-line")) 

569 

570 @handle("enter", filter=~is_returnable & vi_navigation_mode) 

571 def _start_of_next_line(event: E) -> None: 

572 """ 

573 Go to the beginning of next line. 

574 """ 

575 b = event.current_buffer 

576 b.cursor_down(count=event.arg) 

577 b.cursor_position += b.document.get_start_of_line_position( 

578 after_whitespace=True 

579 ) 

580 

581 # ** In navigation mode ** 

582 

583 # List of navigation commands: http://hea-www.harvard.edu/~fine/Tech/vi.html 

584 

585 @handle("insert", filter=vi_navigation_mode) 

586 def _insert_mode(event: E) -> None: 

587 """ 

588 Pressing the Insert key. 

589 """ 

590 event.app.vi_state.input_mode = InputMode.INSERT 

591 

592 @handle("insert", filter=vi_insert_mode) 

593 def _navigation_mode(event: E) -> None: 

594 """ 

595 Pressing the Insert key. 

596 """ 

597 event.app.vi_state.input_mode = InputMode.NAVIGATION 

598 

599 @handle("a", filter=vi_navigation_mode & ~is_read_only) 

600 # ~IsReadOnly, because we want to stay in navigation mode for 

601 # read-only buffers. 

602 def _a(event: E) -> None: 

603 event.current_buffer.cursor_position += ( 

604 event.current_buffer.document.get_cursor_right_position() 

605 ) 

606 event.app.vi_state.input_mode = InputMode.INSERT 

607 

608 @handle("A", filter=vi_navigation_mode & ~is_read_only) 

609 def _A(event: E) -> None: 

610 event.current_buffer.cursor_position += ( 

611 event.current_buffer.document.get_end_of_line_position() 

612 ) 

613 event.app.vi_state.input_mode = InputMode.INSERT 

614 

615 @handle("C", filter=vi_navigation_mode & ~is_read_only) 

616 def _change_until_end_of_line(event: E) -> None: 

617 """ 

618 Change to end of line. 

619 Same as 'c$' (which is implemented elsewhere.) 

620 """ 

621 buffer = event.current_buffer 

622 

623 deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) 

624 event.app.clipboard.set_text(deleted) 

625 event.app.vi_state.input_mode = InputMode.INSERT 

626 

627 @handle("c", "c", filter=vi_navigation_mode & ~is_read_only) 

628 @handle("S", filter=vi_navigation_mode & ~is_read_only) 

629 def _change_current_line(event: E) -> None: # TODO: implement 'arg' 

630 """ 

631 Change current line 

632 """ 

633 buffer = event.current_buffer 

634 

635 # We copy the whole line. 

636 data = ClipboardData(buffer.document.current_line, SelectionType.LINES) 

637 event.app.clipboard.set_data(data) 

638 

639 # But we delete after the whitespace 

640 buffer.cursor_position += buffer.document.get_start_of_line_position( 

641 after_whitespace=True 

642 ) 

643 buffer.delete(count=buffer.document.get_end_of_line_position()) 

644 event.app.vi_state.input_mode = InputMode.INSERT 

645 

646 @handle("D", filter=vi_navigation_mode) 

647 def _delete_until_end_of_line(event: E) -> None: 

648 """ 

649 Delete from cursor position until the end of the line. 

650 """ 

651 buffer = event.current_buffer 

652 deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) 

653 event.app.clipboard.set_text(deleted) 

654 

655 @handle("d", "d", filter=vi_navigation_mode) 

656 def _delete_line(event: E) -> None: 

657 """ 

658 Delete line. (Or the following 'n' lines.) 

659 """ 

660 buffer = event.current_buffer 

661 

662 # Split string in before/deleted/after text. 

663 lines = buffer.document.lines 

664 

665 before = "\n".join(lines[: buffer.document.cursor_position_row]) 

666 deleted = "\n".join( 

667 lines[ 

668 buffer.document.cursor_position_row : buffer.document.cursor_position_row 

669 + event.arg 

670 ] 

671 ) 

672 after = "\n".join(lines[buffer.document.cursor_position_row + event.arg :]) 

673 

674 # Set new text. 

675 if before and after: 

676 before = before + "\n" 

677 

678 # Set text and cursor position. 

679 buffer.document = Document( 

680 text=before + after, 

681 # Cursor At the start of the first 'after' line, after the leading whitespace. 

682 cursor_position=len(before) + len(after) - len(after.lstrip(" ")), 

683 ) 

684 

685 # Set clipboard data 

686 event.app.clipboard.set_data(ClipboardData(deleted, SelectionType.LINES)) 

687 

688 @handle("x", filter=vi_selection_mode) 

689 def _cut(event: E) -> None: 

690 """ 

691 Cut selection. 

692 ('x' is not an operator.) 

693 """ 

694 clipboard_data = event.current_buffer.cut_selection() 

695 event.app.clipboard.set_data(clipboard_data) 

696 

697 @handle("i", filter=vi_navigation_mode & ~is_read_only) 

698 def _i(event: E) -> None: 

699 event.app.vi_state.input_mode = InputMode.INSERT 

700 

701 @handle("I", filter=vi_navigation_mode & ~is_read_only) 

702 def _I(event: E) -> None: 

703 event.app.vi_state.input_mode = InputMode.INSERT 

704 event.current_buffer.cursor_position += ( 

705 event.current_buffer.document.get_start_of_line_position( 

706 after_whitespace=True 

707 ) 

708 ) 

709 

710 @handle("I", filter=in_block_selection & ~is_read_only) 

711 def insert_in_block_selection(event: E, after: bool = False) -> None: 

712 """ 

713 Insert in block selection mode. 

714 """ 

715 buff = event.current_buffer 

716 

717 # Store all cursor positions. 

718 positions = [] 

719 

720 if after: 

721 

722 def get_pos(from_to: tuple[int, int]) -> int: 

723 return from_to[1] 

724 

725 else: 

726 

727 def get_pos(from_to: tuple[int, int]) -> int: 

728 return from_to[0] 

729 

730 for i, from_to in enumerate(buff.document.selection_ranges()): 

731 positions.append(get_pos(from_to)) 

732 if i == 0: 

733 buff.cursor_position = get_pos(from_to) 

734 

735 buff.multiple_cursor_positions = positions 

736 

737 # Go to 'INSERT_MULTIPLE' mode. 

738 event.app.vi_state.input_mode = InputMode.INSERT_MULTIPLE 

739 buff.exit_selection() 

740 

741 @handle("A", filter=in_block_selection & ~is_read_only) 

742 def _append_after_block(event: E) -> None: 

743 insert_in_block_selection(event, after=True) 

744 

745 @handle("J", filter=vi_navigation_mode & ~is_read_only) 

746 def _join(event: E) -> None: 

747 """ 

748 Join lines. 

749 """ 

750 for i in range(event.arg): 

751 event.current_buffer.join_next_line() 

752 

753 @handle("g", "J", filter=vi_navigation_mode & ~is_read_only) 

754 def _join_nospace(event: E) -> None: 

755 """ 

756 Join lines without space. 

757 """ 

758 for i in range(event.arg): 

759 event.current_buffer.join_next_line(separator="") 

760 

761 @handle("J", filter=vi_selection_mode & ~is_read_only) 

762 def _join_selection(event: E) -> None: 

763 """ 

764 Join selected lines. 

765 """ 

766 event.current_buffer.join_selected_lines() 

767 

768 @handle("g", "J", filter=vi_selection_mode & ~is_read_only) 

769 def _join_selection_nospace(event: E) -> None: 

770 """ 

771 Join selected lines without space. 

772 """ 

773 event.current_buffer.join_selected_lines(separator="") 

774 

775 @handle("p", filter=vi_navigation_mode) 

776 def _paste(event: E) -> None: 

777 """ 

778 Paste after 

779 """ 

780 event.current_buffer.paste_clipboard_data( 

781 event.app.clipboard.get_data(), 

782 count=event.arg, 

783 paste_mode=PasteMode.VI_AFTER, 

784 ) 

785 

786 @handle("P", filter=vi_navigation_mode) 

787 def _paste_before(event: E) -> None: 

788 """ 

789 Paste before 

790 """ 

791 event.current_buffer.paste_clipboard_data( 

792 event.app.clipboard.get_data(), 

793 count=event.arg, 

794 paste_mode=PasteMode.VI_BEFORE, 

795 ) 

796 

797 @handle('"', Keys.Any, "p", filter=vi_navigation_mode) 

798 def _paste_register(event: E) -> None: 

799 """ 

800 Paste from named register. 

801 """ 

802 c = event.key_sequence[1].data 

803 if c in vi_register_names: 

804 data = event.app.vi_state.named_registers.get(c) 

805 if data: 

806 event.current_buffer.paste_clipboard_data( 

807 data, count=event.arg, paste_mode=PasteMode.VI_AFTER 

808 ) 

809 

810 @handle('"', Keys.Any, "P", filter=vi_navigation_mode) 

811 def _paste_register_before(event: E) -> None: 

812 """ 

813 Paste (before) from named register. 

814 """ 

815 c = event.key_sequence[1].data 

816 if c in vi_register_names: 

817 data = event.app.vi_state.named_registers.get(c) 

818 if data: 

819 event.current_buffer.paste_clipboard_data( 

820 data, count=event.arg, paste_mode=PasteMode.VI_BEFORE 

821 ) 

822 

823 @handle("r", filter=vi_navigation_mode) 

824 def _replace(event: E) -> None: 

825 """ 

826 Go to 'replace-single'-mode. 

827 """ 

828 event.app.vi_state.input_mode = InputMode.REPLACE_SINGLE 

829 

830 @handle("R", filter=vi_navigation_mode) 

831 def _replace_mode(event: E) -> None: 

832 """ 

833 Go to 'replace'-mode. 

834 """ 

835 event.app.vi_state.input_mode = InputMode.REPLACE 

836 

837 @handle("s", filter=vi_navigation_mode & ~is_read_only) 

838 def _substitute(event: E) -> None: 

839 """ 

840 Substitute with new text 

841 (Delete character(s) and go to insert mode.) 

842 """ 

843 text = event.current_buffer.delete(count=event.arg) 

844 event.app.clipboard.set_text(text) 

845 event.app.vi_state.input_mode = InputMode.INSERT 

846 

847 @handle("u", filter=vi_navigation_mode, save_before=(lambda e: False)) 

848 def _undo(event: E) -> None: 

849 for i in range(event.arg): 

850 event.current_buffer.undo() 

851 

852 @handle("V", filter=vi_navigation_mode) 

853 def _visual_line(event: E) -> None: 

854 """ 

855 Start lines selection. 

856 """ 

857 event.current_buffer.start_selection(selection_type=SelectionType.LINES) 

858 

859 @handle("c-v", filter=vi_navigation_mode) 

860 def _visual_block(event: E) -> None: 

861 """ 

862 Enter block selection mode. 

863 """ 

864 event.current_buffer.start_selection(selection_type=SelectionType.BLOCK) 

865 

866 @handle("V", filter=vi_selection_mode) 

867 def _visual_line2(event: E) -> None: 

868 """ 

869 Exit line selection mode, or go from non line selection mode to line 

870 selection mode. 

871 """ 

872 selection_state = event.current_buffer.selection_state 

873 

874 if selection_state is not None: 

875 if selection_state.type != SelectionType.LINES: 

876 selection_state.type = SelectionType.LINES 

877 else: 

878 event.current_buffer.exit_selection() 

879 

880 @handle("v", filter=vi_navigation_mode) 

881 def _visual(event: E) -> None: 

882 """ 

883 Enter character selection mode. 

884 """ 

885 event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS) 

886 

887 @handle("v", filter=vi_selection_mode) 

888 def _visual2(event: E) -> None: 

889 """ 

890 Exit character selection mode, or go from non-character-selection mode 

891 to character selection mode. 

892 """ 

893 selection_state = event.current_buffer.selection_state 

894 

895 if selection_state is not None: 

896 if selection_state.type != SelectionType.CHARACTERS: 

897 selection_state.type = SelectionType.CHARACTERS 

898 else: 

899 event.current_buffer.exit_selection() 

900 

901 @handle("c-v", filter=vi_selection_mode) 

902 def _visual_block2(event: E) -> None: 

903 """ 

904 Exit block selection mode, or go from non block selection mode to block 

905 selection mode. 

906 """ 

907 selection_state = event.current_buffer.selection_state 

908 

909 if selection_state is not None: 

910 if selection_state.type != SelectionType.BLOCK: 

911 selection_state.type = SelectionType.BLOCK 

912 else: 

913 event.current_buffer.exit_selection() 

914 

915 @handle("a", "w", filter=vi_selection_mode) 

916 @handle("a", "W", filter=vi_selection_mode) 

917 def _visual_auto_word(event: E) -> None: 

918 """ 

919 Switch from visual linewise mode to visual characterwise mode. 

920 """ 

921 buffer = event.current_buffer 

922 

923 if ( 

924 buffer.selection_state 

925 and buffer.selection_state.type == SelectionType.LINES 

926 ): 

927 buffer.selection_state.type = SelectionType.CHARACTERS 

928 

929 @handle("x", filter=vi_navigation_mode) 

930 def _delete(event: E) -> None: 

931 """ 

932 Delete character. 

933 """ 

934 buff = event.current_buffer 

935 count = min(event.arg, len(buff.document.current_line_after_cursor)) 

936 if count: 

937 text = event.current_buffer.delete(count=count) 

938 event.app.clipboard.set_text(text) 

939 

940 @handle("X", filter=vi_navigation_mode) 

941 def _delete_before_cursor(event: E) -> None: 

942 buff = event.current_buffer 

943 count = min(event.arg, len(buff.document.current_line_before_cursor)) 

944 if count: 

945 text = event.current_buffer.delete_before_cursor(count=count) 

946 event.app.clipboard.set_text(text) 

947 

948 @handle("y", "y", filter=vi_navigation_mode) 

949 @handle("Y", filter=vi_navigation_mode) 

950 def _yank_line(event: E) -> None: 

951 """ 

952 Yank the whole line. 

953 """ 

954 text = "\n".join(event.current_buffer.document.lines_from_current[: event.arg]) 

955 event.app.clipboard.set_data(ClipboardData(text, SelectionType.LINES)) 

956 

957 @handle("+", filter=vi_navigation_mode) 

958 def _next_line(event: E) -> None: 

959 """ 

960 Move to first non whitespace of next line 

961 """ 

962 buffer = event.current_buffer 

963 buffer.cursor_position += buffer.document.get_cursor_down_position( 

964 count=event.arg 

965 ) 

966 buffer.cursor_position += buffer.document.get_start_of_line_position( 

967 after_whitespace=True 

968 ) 

969 

970 @handle("-", filter=vi_navigation_mode) 

971 def _prev_line(event: E) -> None: 

972 """ 

973 Move to first non whitespace of previous line 

974 """ 

975 buffer = event.current_buffer 

976 buffer.cursor_position += buffer.document.get_cursor_up_position( 

977 count=event.arg 

978 ) 

979 buffer.cursor_position += buffer.document.get_start_of_line_position( 

980 after_whitespace=True 

981 ) 

982 

983 @handle(">", ">", filter=vi_navigation_mode) 

984 @handle("c-t", filter=vi_insert_mode) 

985 def _indent(event: E) -> None: 

986 """ 

987 Indent lines. 

988 """ 

989 buffer = event.current_buffer 

990 current_row = buffer.document.cursor_position_row 

991 indent(buffer, current_row, current_row + event.arg) 

992 

993 @handle("<", "<", filter=vi_navigation_mode) 

994 @handle("c-d", filter=vi_insert_mode) 

995 def _unindent(event: E) -> None: 

996 """ 

997 Unindent lines. 

998 """ 

999 current_row = event.current_buffer.document.cursor_position_row 

1000 unindent(event.current_buffer, current_row, current_row + event.arg) 

1001 

1002 @handle("O", filter=vi_navigation_mode & ~is_read_only) 

1003 def _open_above(event: E) -> None: 

1004 """ 

1005 Open line above and enter insertion mode 

1006 """ 

1007 event.current_buffer.insert_line_above(copy_margin=not in_paste_mode()) 

1008 event.app.vi_state.input_mode = InputMode.INSERT 

1009 

1010 @handle("o", filter=vi_navigation_mode & ~is_read_only) 

1011 def _open_below(event: E) -> None: 

1012 """ 

1013 Open line below and enter insertion mode 

1014 """ 

1015 event.current_buffer.insert_line_below(copy_margin=not in_paste_mode()) 

1016 event.app.vi_state.input_mode = InputMode.INSERT 

1017 

1018 @handle("~", filter=vi_navigation_mode) 

1019 def _reverse_case(event: E) -> None: 

1020 """ 

1021 Reverse case of current character and move cursor forward. 

1022 """ 

1023 buffer = event.current_buffer 

1024 c = buffer.document.current_char 

1025 

1026 if c is not None and c != "\n": 

1027 buffer.insert_text(c.swapcase(), overwrite=True) 

1028 

1029 @handle("g", "u", "u", filter=vi_navigation_mode & ~is_read_only) 

1030 def _lowercase_line(event: E) -> None: 

1031 """ 

1032 Lowercase current line. 

1033 """ 

1034 buff = event.current_buffer 

1035 buff.transform_current_line(lambda s: s.lower()) 

1036 

1037 @handle("g", "U", "U", filter=vi_navigation_mode & ~is_read_only) 

1038 def _uppercase_line(event: E) -> None: 

1039 """ 

1040 Uppercase current line. 

1041 """ 

1042 buff = event.current_buffer 

1043 buff.transform_current_line(lambda s: s.upper()) 

1044 

1045 @handle("g", "~", "~", filter=vi_navigation_mode & ~is_read_only) 

1046 def _swapcase_line(event: E) -> None: 

1047 """ 

1048 Swap case of the current line. 

1049 """ 

1050 buff = event.current_buffer 

1051 buff.transform_current_line(lambda s: s.swapcase()) 

1052 

1053 @handle("#", filter=vi_navigation_mode) 

1054 def _prev_occurrence(event: E) -> None: 

1055 """ 

1056 Go to previous occurrence of this word. 

1057 """ 

1058 b = event.current_buffer 

1059 search_state = event.app.current_search_state 

1060 

1061 search_state.text = b.document.get_word_under_cursor() 

1062 search_state.direction = SearchDirection.BACKWARD 

1063 

1064 b.apply_search(search_state, count=event.arg, include_current_position=False) 

1065 

1066 @handle("*", filter=vi_navigation_mode) 

1067 def _next_occurrence(event: E) -> None: 

1068 """ 

1069 Go to next occurrence of this word. 

1070 """ 

1071 b = event.current_buffer 

1072 search_state = event.app.current_search_state 

1073 

1074 search_state.text = b.document.get_word_under_cursor() 

1075 search_state.direction = SearchDirection.FORWARD 

1076 

1077 b.apply_search(search_state, count=event.arg, include_current_position=False) 

1078 

1079 @handle("(", filter=vi_navigation_mode) 

1080 def _begin_of_sentence(event: E) -> None: 

1081 # TODO: go to begin of sentence. 

1082 # XXX: should become text_object. 

1083 pass 

1084 

1085 @handle(")", filter=vi_navigation_mode) 

1086 def _end_of_sentence(event: E) -> None: 

1087 # TODO: go to end of sentence. 

1088 # XXX: should become text_object. 

1089 pass 

1090 

1091 operator = create_operator_decorator(key_bindings) 

1092 text_object = create_text_object_decorator(key_bindings) 

1093 

1094 @handle(Keys.Any, filter=vi_waiting_for_text_object_mode) 

1095 def _unknown_text_object(event: E) -> None: 

1096 """ 

1097 Unknown key binding while waiting for a text object. 

1098 """ 

1099 event.app.output.bell() 

1100 

1101 # 

1102 # *** Operators *** 

1103 # 

1104 

1105 def create_delete_and_change_operators( 

1106 delete_only: bool, with_register: bool = False 

1107 ) -> None: 

1108 """ 

1109 Delete and change operators. 

1110 

1111 :param delete_only: Create an operator that deletes, but doesn't go to insert mode. 

1112 :param with_register: Copy the deleted text to this named register instead of the clipboard. 

1113 """ 

1114 handler_keys: Iterable[str] 

1115 if with_register: 

1116 handler_keys = ('"', Keys.Any, "cd"[delete_only]) 

1117 else: 

1118 handler_keys = "cd"[delete_only] 

1119 

1120 @operator(*handler_keys, filter=~is_read_only) 

1121 def delete_or_change_operator(event: E, text_object: TextObject) -> None: 

1122 clipboard_data = None 

1123 buff = event.current_buffer 

1124 

1125 if text_object: 

1126 new_document, clipboard_data = text_object.cut(buff) 

1127 buff.document = new_document 

1128 

1129 # Set deleted/changed text to clipboard or named register. 

1130 if clipboard_data and clipboard_data.text: 

1131 if with_register: 

1132 reg_name = event.key_sequence[1].data 

1133 if reg_name in vi_register_names: 

1134 event.app.vi_state.named_registers[reg_name] = clipboard_data 

1135 else: 

1136 event.app.clipboard.set_data(clipboard_data) 

1137 

1138 # Only go back to insert mode in case of 'change'. 

1139 if not delete_only: 

1140 event.app.vi_state.input_mode = InputMode.INSERT 

1141 

1142 create_delete_and_change_operators(False, False) 

1143 create_delete_and_change_operators(False, True) 

1144 create_delete_and_change_operators(True, False) 

1145 create_delete_and_change_operators(True, True) 

1146 

1147 def create_transform_handler( 

1148 filter: Filter, transform_func: Callable[[str], str], *a: str 

1149 ) -> None: 

1150 @operator(*a, filter=filter & ~is_read_only) 

1151 def _(event: E, text_object: TextObject) -> None: 

1152 """ 

1153 Apply transformation (uppercase, lowercase, rot13, swap case). 

1154 """ 

1155 buff = event.current_buffer 

1156 start, end = text_object.operator_range(buff.document) 

1157 

1158 if start < end: 

1159 # Transform. 

1160 buff.transform_region( 

1161 buff.cursor_position + start, 

1162 buff.cursor_position + end, 

1163 transform_func, 

1164 ) 

1165 

1166 # Move cursor 

1167 buff.cursor_position += text_object.end or text_object.start 

1168 

1169 for k, f, func in vi_transform_functions: 

1170 create_transform_handler(f, func, *k) 

1171 

1172 @operator("y") 

1173 def _yank(event: E, text_object: TextObject) -> None: 

1174 """ 

1175 Yank operator. (Copy text.) 

1176 """ 

1177 _, clipboard_data = text_object.cut(event.current_buffer) 

1178 if clipboard_data.text: 

1179 event.app.clipboard.set_data(clipboard_data) 

1180 

1181 @operator('"', Keys.Any, "y") 

1182 def _yank_to_register(event: E, text_object: TextObject) -> None: 

1183 """ 

1184 Yank selection to named register. 

1185 """ 

1186 c = event.key_sequence[1].data 

1187 if c in vi_register_names: 

1188 _, clipboard_data = text_object.cut(event.current_buffer) 

1189 event.app.vi_state.named_registers[c] = clipboard_data 

1190 

1191 @operator(">") 

1192 def _indent_text_object(event: E, text_object: TextObject) -> None: 

1193 """ 

1194 Indent. 

1195 """ 

1196 buff = event.current_buffer 

1197 from_, to = text_object.get_line_numbers(buff) 

1198 indent(buff, from_, to + 1, count=event.arg) 

1199 

1200 @operator("<") 

1201 def _unindent_text_object(event: E, text_object: TextObject) -> None: 

1202 """ 

1203 Unindent. 

1204 """ 

1205 buff = event.current_buffer 

1206 from_, to = text_object.get_line_numbers(buff) 

1207 unindent(buff, from_, to + 1, count=event.arg) 

1208 

1209 @operator("g", "q") 

1210 def _reshape(event: E, text_object: TextObject) -> None: 

1211 """ 

1212 Reshape text. 

1213 """ 

1214 buff = event.current_buffer 

1215 from_, to = text_object.get_line_numbers(buff) 

1216 reshape_text(buff, from_, to) 

1217 

1218 # 

1219 # *** Text objects *** 

1220 # 

1221 

1222 @text_object("b") 

1223 def _b(event: E) -> TextObject: 

1224 """ 

1225 Move one word or token left. 

1226 """ 

1227 return TextObject( 

1228 event.current_buffer.document.find_start_of_previous_word(count=event.arg) 

1229 or 0 

1230 ) 

1231 

1232 @text_object("B") 

1233 def _B(event: E) -> TextObject: 

1234 """ 

1235 Move one non-blank word left 

1236 """ 

1237 return TextObject( 

1238 event.current_buffer.document.find_start_of_previous_word( 

1239 count=event.arg, WORD=True 

1240 ) 

1241 or 0 

1242 ) 

1243 

1244 @text_object("$") 

1245 def _dollar(event: E) -> TextObject: 

1246 """ 

1247 'c$', 'd$' and '$': Delete/change/move until end of line. 

1248 """ 

1249 return TextObject(event.current_buffer.document.get_end_of_line_position()) 

1250 

1251 @text_object("w") 

1252 def _word_forward(event: E) -> TextObject: 

1253 """ 

1254 'word' forward. 'cw', 'dw', 'w': Delete/change/move one word. 

1255 """ 

1256 return TextObject( 

1257 event.current_buffer.document.find_next_word_beginning(count=event.arg) 

1258 or event.current_buffer.document.get_end_of_document_position() 

1259 ) 

1260 

1261 @text_object("W") 

1262 def _WORD_forward(event: E) -> TextObject: 

1263 """ 

1264 'WORD' forward. 'cW', 'dW', 'W': Delete/change/move one WORD. 

1265 """ 

1266 return TextObject( 

1267 event.current_buffer.document.find_next_word_beginning( 

1268 count=event.arg, WORD=True 

1269 ) 

1270 or event.current_buffer.document.get_end_of_document_position() 

1271 ) 

1272 

1273 @text_object("e") 

1274 def _end_of_word(event: E) -> TextObject: 

1275 """ 

1276 End of 'word': 'ce', 'de', 'e' 

1277 """ 

1278 end = event.current_buffer.document.find_next_word_ending(count=event.arg) 

1279 return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) 

1280 

1281 @text_object("E") 

1282 def _end_of_WORD(event: E) -> TextObject: 

1283 """ 

1284 End of 'WORD': 'cE', 'dE', 'E' 

1285 """ 

1286 end = event.current_buffer.document.find_next_word_ending( 

1287 count=event.arg, WORD=True 

1288 ) 

1289 return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) 

1290 

1291 @text_object("i", "w", no_move_handler=True) 

1292 def _inner_word(event: E) -> TextObject: 

1293 """ 

1294 Inner 'word': ciw and diw 

1295 """ 

1296 start, end = event.current_buffer.document.find_boundaries_of_current_word() 

1297 return TextObject(start, end) 

1298 

1299 @text_object("a", "w", no_move_handler=True) 

1300 def _a_word(event: E) -> TextObject: 

1301 """ 

1302 A 'word': caw and daw 

1303 """ 

1304 start, end = event.current_buffer.document.find_boundaries_of_current_word( 

1305 include_trailing_whitespace=True 

1306 ) 

1307 return TextObject(start, end) 

1308 

1309 @text_object("i", "W", no_move_handler=True) 

1310 def _inner_WORD(event: E) -> TextObject: 

1311 """ 

1312 Inner 'WORD': ciW and diW 

1313 """ 

1314 start, end = event.current_buffer.document.find_boundaries_of_current_word( 

1315 WORD=True 

1316 ) 

1317 return TextObject(start, end) 

1318 

1319 @text_object("a", "W", no_move_handler=True) 

1320 def _a_WORD(event: E) -> TextObject: 

1321 """ 

1322 A 'WORD': caw and daw 

1323 """ 

1324 start, end = event.current_buffer.document.find_boundaries_of_current_word( 

1325 WORD=True, include_trailing_whitespace=True 

1326 ) 

1327 return TextObject(start, end) 

1328 

1329 @text_object("a", "p", no_move_handler=True) 

1330 def _paragraph(event: E) -> TextObject: 

1331 """ 

1332 Auto paragraph. 

1333 """ 

1334 start = event.current_buffer.document.start_of_paragraph() 

1335 end = event.current_buffer.document.end_of_paragraph(count=event.arg) 

1336 return TextObject(start, end) 

1337 

1338 @text_object("^") 

1339 def _start_of_line(event: E) -> TextObject: 

1340 """'c^', 'd^' and '^': Soft start of line, after whitespace.""" 

1341 return TextObject( 

1342 event.current_buffer.document.get_start_of_line_position( 

1343 after_whitespace=True 

1344 ) 

1345 ) 

1346 

1347 @text_object("0") 

1348 def _hard_start_of_line(event: E) -> TextObject: 

1349 """ 

1350 'c0', 'd0': Hard start of line, before whitespace. 

1351 (The move '0' key is implemented elsewhere, because a '0' could also change the `arg`.) 

1352 """ 

1353 return TextObject( 

1354 event.current_buffer.document.get_start_of_line_position( 

1355 after_whitespace=False 

1356 ) 

1357 ) 

1358 

1359 def create_ci_ca_handles( 

1360 ci_start: str, ci_end: str, inner: bool, key: str | None = None 

1361 ) -> None: 

1362 # TODO: 'dat', 'dit', (tags (like xml) 

1363 """ 

1364 Delete/Change string between this start and stop character. But keep these characters. 

1365 This implements all the ci", ci<, ci{, ci(, di", di<, ca", ca<, ... combinations. 

1366 """ 

1367 

1368 def handler(event: E) -> TextObject: 

1369 if ci_start == ci_end: 

1370 # Quotes 

1371 start = event.current_buffer.document.find_backwards( 

1372 ci_start, in_current_line=False 

1373 ) 

1374 end = event.current_buffer.document.find(ci_end, in_current_line=False) 

1375 else: 

1376 # Brackets 

1377 start = event.current_buffer.document.find_enclosing_bracket_left( 

1378 ci_start, ci_end 

1379 ) 

1380 end = event.current_buffer.document.find_enclosing_bracket_right( 

1381 ci_start, ci_end 

1382 ) 

1383 

1384 if start is not None and end is not None: 

1385 offset = 0 if inner else 1 

1386 return TextObject(start + 1 - offset, end + offset) 

1387 else: 

1388 # Nothing found. 

1389 return TextObject(0) 

1390 

1391 if key is None: 

1392 text_object("ai"[inner], ci_start, no_move_handler=True)(handler) 

1393 text_object("ai"[inner], ci_end, no_move_handler=True)(handler) 

1394 else: 

1395 text_object("ai"[inner], key, no_move_handler=True)(handler) 

1396 

1397 for inner in (False, True): 

1398 for ci_start, ci_end in [ 

1399 ('"', '"'), 

1400 ("'", "'"), 

1401 ("`", "`"), 

1402 ("[", "]"), 

1403 ("<", ">"), 

1404 ("{", "}"), 

1405 ("(", ")"), 

1406 ]: 

1407 create_ci_ca_handles(ci_start, ci_end, inner) 

1408 

1409 create_ci_ca_handles("(", ")", inner, "b") # 'dab', 'dib' 

1410 create_ci_ca_handles("{", "}", inner, "B") # 'daB', 'diB' 

1411 

1412 @text_object("{") 

1413 def _previous_section(event: E) -> TextObject: 

1414 """ 

1415 Move to previous blank-line separated section. 

1416 Implements '{', 'c{', 'd{', 'y{' 

1417 """ 

1418 index = event.current_buffer.document.start_of_paragraph( 

1419 count=event.arg, before=True 

1420 ) 

1421 return TextObject(index) 

1422 

1423 @text_object("}") 

1424 def _next_section(event: E) -> TextObject: 

1425 """ 

1426 Move to next blank-line separated section. 

1427 Implements '}', 'c}', 'd}', 'y}' 

1428 """ 

1429 index = event.current_buffer.document.end_of_paragraph( 

1430 count=event.arg, after=True 

1431 ) 

1432 return TextObject(index) 

1433 

1434 @text_object("f", Keys.Any) 

1435 def _find_next_occurrence(event: E) -> TextObject: 

1436 """ 

1437 Go to next occurrence of character. Typing 'fx' will move the 

1438 cursor to the next occurrence of character. 'x'. 

1439 """ 

1440 event.app.vi_state.last_character_find = CharacterFind(event.data, False) 

1441 match = event.current_buffer.document.find( 

1442 event.data, in_current_line=True, count=event.arg 

1443 ) 

1444 if match: 

1445 return TextObject(match, type=TextObjectType.INCLUSIVE) 

1446 else: 

1447 return TextObject(0) 

1448 

1449 @text_object("F", Keys.Any) 

1450 def _find_previous_occurrence(event: E) -> TextObject: 

1451 """ 

1452 Go to previous occurrence of character. Typing 'Fx' will move the 

1453 cursor to the previous occurrence of character. 'x'. 

1454 """ 

1455 event.app.vi_state.last_character_find = CharacterFind(event.data, True) 

1456 return TextObject( 

1457 event.current_buffer.document.find_backwards( 

1458 event.data, in_current_line=True, count=event.arg 

1459 ) 

1460 or 0 

1461 ) 

1462 

1463 @text_object("t", Keys.Any) 

1464 def _t(event: E) -> TextObject: 

1465 """ 

1466 Move right to the next occurrence of c, then one char backward. 

1467 """ 

1468 event.app.vi_state.last_character_find = CharacterFind(event.data, False) 

1469 match = event.current_buffer.document.find( 

1470 event.data, in_current_line=True, count=event.arg 

1471 ) 

1472 if match: 

1473 return TextObject(match - 1, type=TextObjectType.INCLUSIVE) 

1474 else: 

1475 return TextObject(0) 

1476 

1477 @text_object("T", Keys.Any) 

1478 def _T(event: E) -> TextObject: 

1479 """ 

1480 Move left to the previous occurrence of c, then one char forward. 

1481 """ 

1482 event.app.vi_state.last_character_find = CharacterFind(event.data, True) 

1483 match = event.current_buffer.document.find_backwards( 

1484 event.data, in_current_line=True, count=event.arg 

1485 ) 

1486 return TextObject(match + 1 if match else 0) 

1487 

1488 def repeat(reverse: bool) -> None: 

1489 """ 

1490 Create ',' and ';' commands. 

1491 """ 

1492 

1493 @text_object("," if reverse else ";") 

1494 def _(event: E) -> TextObject: 

1495 """ 

1496 Repeat the last 'f'/'F'/'t'/'T' command. 

1497 """ 

1498 pos: int | None = 0 

1499 vi_state = event.app.vi_state 

1500 

1501 type = TextObjectType.EXCLUSIVE 

1502 

1503 if vi_state.last_character_find: 

1504 char = vi_state.last_character_find.character 

1505 backwards = vi_state.last_character_find.backwards 

1506 

1507 if reverse: 

1508 backwards = not backwards 

1509 

1510 if backwards: 

1511 pos = event.current_buffer.document.find_backwards( 

1512 char, in_current_line=True, count=event.arg 

1513 ) 

1514 else: 

1515 pos = event.current_buffer.document.find( 

1516 char, in_current_line=True, count=event.arg 

1517 ) 

1518 type = TextObjectType.INCLUSIVE 

1519 if pos: 

1520 return TextObject(pos, type=type) 

1521 else: 

1522 return TextObject(0) 

1523 

1524 repeat(True) 

1525 repeat(False) 

1526 

1527 @text_object("h") 

1528 @text_object("left") 

1529 def _left(event: E) -> TextObject: 

1530 """ 

1531 Implements 'ch', 'dh', 'h': Cursor left. 

1532 """ 

1533 return TextObject( 

1534 event.current_buffer.document.get_cursor_left_position(count=event.arg) 

1535 ) 

1536 

1537 @text_object("j", no_move_handler=True, no_selection_handler=True) 

1538 # Note: We also need `no_selection_handler`, because we in 

1539 # selection mode, we prefer the other 'j' binding that keeps 

1540 # `buffer.preferred_column`. 

1541 def _down(event: E) -> TextObject: 

1542 """ 

1543 Implements 'cj', 'dj', 'j', ... Cursor up. 

1544 """ 

1545 return TextObject( 

1546 event.current_buffer.document.get_cursor_down_position(count=event.arg), 

1547 type=TextObjectType.LINEWISE, 

1548 ) 

1549 

1550 @text_object("k", no_move_handler=True, no_selection_handler=True) 

1551 def _up(event: E) -> TextObject: 

1552 """ 

1553 Implements 'ck', 'dk', 'k', ... Cursor up. 

1554 """ 

1555 return TextObject( 

1556 event.current_buffer.document.get_cursor_up_position(count=event.arg), 

1557 type=TextObjectType.LINEWISE, 

1558 ) 

1559 

1560 @text_object("l") 

1561 @text_object(" ") 

1562 @text_object("right") 

1563 def _right(event: E) -> TextObject: 

1564 """ 

1565 Implements 'cl', 'dl', 'l', 'c ', 'd ', ' '. Cursor right. 

1566 """ 

1567 return TextObject( 

1568 event.current_buffer.document.get_cursor_right_position(count=event.arg) 

1569 ) 

1570 

1571 @text_object("H") 

1572 def _top_of_screen(event: E) -> TextObject: 

1573 """ 

1574 Moves to the start of the visible region. (Below the scroll offset.) 

1575 Implements 'cH', 'dH', 'H'. 

1576 """ 

1577 w = event.app.layout.current_window 

1578 b = event.current_buffer 

1579 

1580 if w and w.render_info: 

1581 # When we find a Window that has BufferControl showing this window, 

1582 # move to the start of the visible area. 

1583 pos = ( 

1584 b.document.translate_row_col_to_index( 

1585 w.render_info.first_visible_line(after_scroll_offset=True), 0 

1586 ) 

1587 - b.cursor_position 

1588 ) 

1589 

1590 else: 

1591 # Otherwise, move to the start of the input. 

1592 pos = -len(b.document.text_before_cursor) 

1593 return TextObject(pos, type=TextObjectType.LINEWISE) 

1594 

1595 @text_object("M") 

1596 def _middle_of_screen(event: E) -> TextObject: 

1597 """ 

1598 Moves cursor to the vertical center of the visible region. 

1599 Implements 'cM', 'dM', 'M'. 

1600 """ 

1601 w = event.app.layout.current_window 

1602 b = event.current_buffer 

1603 

1604 if w and w.render_info: 

1605 # When we find a Window that has BufferControl showing this window, 

1606 # move to the center of the visible area. 

1607 pos = ( 

1608 b.document.translate_row_col_to_index( 

1609 w.render_info.center_visible_line(), 0 

1610 ) 

1611 - b.cursor_position 

1612 ) 

1613 

1614 else: 

1615 # Otherwise, move to the start of the input. 

1616 pos = -len(b.document.text_before_cursor) 

1617 return TextObject(pos, type=TextObjectType.LINEWISE) 

1618 

1619 @text_object("L") 

1620 def _end_of_screen(event: E) -> TextObject: 

1621 """ 

1622 Moves to the end of the visible region. (Above the scroll offset.) 

1623 """ 

1624 w = event.app.layout.current_window 

1625 b = event.current_buffer 

1626 

1627 if w and w.render_info: 

1628 # When we find a Window that has BufferControl showing this window, 

1629 # move to the end of the visible area. 

1630 pos = ( 

1631 b.document.translate_row_col_to_index( 

1632 w.render_info.last_visible_line(before_scroll_offset=True), 0 

1633 ) 

1634 - b.cursor_position 

1635 ) 

1636 

1637 else: 

1638 # Otherwise, move to the end of the input. 

1639 pos = len(b.document.text_after_cursor) 

1640 return TextObject(pos, type=TextObjectType.LINEWISE) 

1641 

1642 @text_object("n", no_move_handler=True) 

1643 def _search_next(event: E) -> TextObject: 

1644 """ 

1645 Search next. 

1646 """ 

1647 buff = event.current_buffer 

1648 search_state = event.app.current_search_state 

1649 

1650 cursor_position = buff.get_search_position( 

1651 search_state, include_current_position=False, count=event.arg 

1652 ) 

1653 return TextObject(cursor_position - buff.cursor_position) 

1654 

1655 @handle("n", filter=vi_navigation_mode) 

1656 def _search_next2(event: E) -> None: 

1657 """ 

1658 Search next in navigation mode. (This goes through the history.) 

1659 """ 

1660 search_state = event.app.current_search_state 

1661 

1662 event.current_buffer.apply_search( 

1663 search_state, include_current_position=False, count=event.arg 

1664 ) 

1665 

1666 @text_object("N", no_move_handler=True) 

1667 def _search_previous(event: E) -> TextObject: 

1668 """ 

1669 Search previous. 

1670 """ 

1671 buff = event.current_buffer 

1672 search_state = event.app.current_search_state 

1673 

1674 cursor_position = buff.get_search_position( 

1675 ~search_state, include_current_position=False, count=event.arg 

1676 ) 

1677 return TextObject(cursor_position - buff.cursor_position) 

1678 

1679 @handle("N", filter=vi_navigation_mode) 

1680 def _search_previous2(event: E) -> None: 

1681 """ 

1682 Search previous in navigation mode. (This goes through the history.) 

1683 """ 

1684 search_state = event.app.current_search_state 

1685 

1686 event.current_buffer.apply_search( 

1687 ~search_state, include_current_position=False, count=event.arg 

1688 ) 

1689 

1690 @handle("z", "+", filter=vi_navigation_mode | vi_selection_mode) 

1691 @handle("z", "t", filter=vi_navigation_mode | vi_selection_mode) 

1692 @handle("z", "enter", filter=vi_navigation_mode | vi_selection_mode) 

1693 def _scroll_top(event: E) -> None: 

1694 """ 

1695 Scrolls the window to makes the current line the first line in the visible region. 

1696 """ 

1697 b = event.current_buffer 

1698 event.app.layout.current_window.vertical_scroll = b.document.cursor_position_row 

1699 

1700 @handle("z", "-", filter=vi_navigation_mode | vi_selection_mode) 

1701 @handle("z", "b", filter=vi_navigation_mode | vi_selection_mode) 

1702 def _scroll_bottom(event: E) -> None: 

1703 """ 

1704 Scrolls the window to makes the current line the last line in the visible region. 

1705 """ 

1706 # We can safely set the scroll offset to zero; the Window will make 

1707 # sure that it scrolls at least enough to make the cursor visible 

1708 # again. 

1709 event.app.layout.current_window.vertical_scroll = 0 

1710 

1711 @handle("z", "z", filter=vi_navigation_mode | vi_selection_mode) 

1712 def _scroll_center(event: E) -> None: 

1713 """ 

1714 Center Window vertically around cursor. 

1715 """ 

1716 w = event.app.layout.current_window 

1717 b = event.current_buffer 

1718 

1719 if w and w.render_info: 

1720 info = w.render_info 

1721 

1722 # Calculate the offset that we need in order to position the row 

1723 # containing the cursor in the center. 

1724 scroll_height = info.window_height // 2 

1725 

1726 y = max(0, b.document.cursor_position_row - 1) 

1727 height = 0 

1728 while y > 0: 

1729 line_height = info.get_height_for_line(y) 

1730 

1731 if height + line_height < scroll_height: 

1732 height += line_height 

1733 y -= 1 

1734 else: 

1735 break 

1736 

1737 w.vertical_scroll = y 

1738 

1739 @text_object("%") 

1740 def _goto_corresponding_bracket(event: E) -> TextObject: 

1741 """ 

1742 Implements 'c%', 'd%', '%, 'y%' (Move to corresponding bracket.) 

1743 If an 'arg' has been given, go this this % position in the file. 

1744 """ 

1745 buffer = event.current_buffer 

1746 

1747 if event._arg: 

1748 # If 'arg' has been given, the meaning of % is to go to the 'x%' 

1749 # row in the file. 

1750 if 0 < event.arg <= 100: 

1751 absolute_index = buffer.document.translate_row_col_to_index( 

1752 int((event.arg * buffer.document.line_count - 1) / 100), 0 

1753 ) 

1754 return TextObject( 

1755 absolute_index - buffer.document.cursor_position, 

1756 type=TextObjectType.LINEWISE, 

1757 ) 

1758 else: 

1759 return TextObject(0) # Do nothing. 

1760 

1761 else: 

1762 # Move to the corresponding opening/closing bracket (()'s, []'s and {}'s). 

1763 match = buffer.document.find_matching_bracket_position() 

1764 if match: 

1765 return TextObject(match, type=TextObjectType.INCLUSIVE) 

1766 else: 

1767 return TextObject(0) 

1768 

1769 @text_object("|") 

1770 def _to_column(event: E) -> TextObject: 

1771 """ 

1772 Move to the n-th column (you may specify the argument n by typing it on 

1773 number keys, for example, 20|). 

1774 """ 

1775 return TextObject( 

1776 event.current_buffer.document.get_column_cursor_position(event.arg - 1) 

1777 ) 

1778 

1779 @text_object("g", "g") 

1780 def _goto_first_line(event: E) -> TextObject: 

1781 """ 

1782 Go to the start of the very first line. 

1783 Implements 'gg', 'cgg', 'ygg' 

1784 """ 

1785 d = event.current_buffer.document 

1786 

1787 if event._arg: 

1788 # Move to the given line. 

1789 return TextObject( 

1790 d.translate_row_col_to_index(event.arg - 1, 0) - d.cursor_position, 

1791 type=TextObjectType.LINEWISE, 

1792 ) 

1793 else: 

1794 # Move to the top of the input. 

1795 return TextObject( 

1796 d.get_start_of_document_position(), type=TextObjectType.LINEWISE 

1797 ) 

1798 

1799 @text_object("g", "_") 

1800 def _goto_last_line(event: E) -> TextObject: 

1801 """ 

1802 Go to last non-blank of line. 

1803 'g_', 'cg_', 'yg_', etc.. 

1804 """ 

1805 return TextObject( 

1806 event.current_buffer.document.last_non_blank_of_current_line_position(), 

1807 type=TextObjectType.INCLUSIVE, 

1808 ) 

1809 

1810 @text_object("g", "e") 

1811 def _ge(event: E) -> TextObject: 

1812 """ 

1813 Go to last character of previous word. 

1814 'ge', 'cge', 'yge', etc.. 

1815 """ 

1816 prev_end = event.current_buffer.document.find_previous_word_ending( 

1817 count=event.arg 

1818 ) 

1819 return TextObject( 

1820 prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE 

1821 ) 

1822 

1823 @text_object("g", "E") 

1824 def _gE(event: E) -> TextObject: 

1825 """ 

1826 Go to last character of previous WORD. 

1827 'gE', 'cgE', 'ygE', etc.. 

1828 """ 

1829 prev_end = event.current_buffer.document.find_previous_word_ending( 

1830 count=event.arg, WORD=True 

1831 ) 

1832 return TextObject( 

1833 prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE 

1834 ) 

1835 

1836 @text_object("g", "m") 

1837 def _gm(event: E) -> TextObject: 

1838 """ 

1839 Like g0, but half a screenwidth to the right. (Or as much as possible.) 

1840 """ 

1841 w = event.app.layout.current_window 

1842 buff = event.current_buffer 

1843 

1844 if w and w.render_info: 

1845 width = w.render_info.window_width 

1846 start = buff.document.get_start_of_line_position(after_whitespace=False) 

1847 start += int(min(width / 2, len(buff.document.current_line))) 

1848 

1849 return TextObject(start, type=TextObjectType.INCLUSIVE) 

1850 return TextObject(0) 

1851 

1852 @text_object("G") 

1853 def _last_line(event: E) -> TextObject: 

1854 """ 

1855 Go to the end of the document. (If no arg has been given.) 

1856 """ 

1857 buf = event.current_buffer 

1858 return TextObject( 

1859 buf.document.translate_row_col_to_index(buf.document.line_count - 1, 0) 

1860 - buf.cursor_position, 

1861 type=TextObjectType.LINEWISE, 

1862 ) 

1863 

1864 # 

1865 # *** Other *** 

1866 # 

1867 

1868 @handle("G", filter=has_arg) 

1869 def _to_nth_history_line(event: E) -> None: 

1870 """ 

1871 If an argument is given, move to this line in the history. (for 

1872 example, 15G) 

1873 """ 

1874 event.current_buffer.go_to_history(event.arg - 1) 

1875 

1876 for n in "123456789": 

1877 

1878 @handle( 

1879 n, 

1880 filter=vi_navigation_mode 

1881 | vi_selection_mode 

1882 | vi_waiting_for_text_object_mode, 

1883 ) 

1884 def _arg(event: E) -> None: 

1885 """ 

1886 Always handle numerics in navigation mode as arg. 

1887 """ 

1888 event.append_to_arg_count(event.data) 

1889 

1890 @handle( 

1891 "0", 

1892 filter=( 

1893 vi_navigation_mode | vi_selection_mode | vi_waiting_for_text_object_mode 

1894 ) 

1895 & has_arg, 

1896 ) 

1897 def _0_arg(event: E) -> None: 

1898 """ 

1899 Zero when an argument was already give. 

1900 """ 

1901 event.append_to_arg_count(event.data) 

1902 

1903 @handle(Keys.Any, filter=vi_replace_mode) 

1904 def _insert_text(event: E) -> None: 

1905 """ 

1906 Insert data at cursor position. 

1907 """ 

1908 event.current_buffer.insert_text(event.data, overwrite=True) 

1909 

1910 @handle(Keys.Any, filter=vi_replace_single_mode) 

1911 def _replace_single(event: E) -> None: 

1912 """ 

1913 Replace single character at cursor position. 

1914 """ 

1915 event.current_buffer.insert_text(event.data, overwrite=True) 

1916 event.current_buffer.cursor_position -= 1 

1917 event.app.vi_state.input_mode = InputMode.NAVIGATION 

1918 

1919 @handle( 

1920 Keys.Any, 

1921 filter=vi_insert_multiple_mode, 

1922 save_before=(lambda e: not e.is_repeat), 

1923 ) 

1924 def _insert_text_multiple_cursors(event: E) -> None: 

1925 """ 

1926 Insert data at multiple cursor positions at once. 

1927 (Usually a result of pressing 'I' or 'A' in block-selection mode.) 

1928 """ 

1929 buff = event.current_buffer 

1930 original_text = buff.text 

1931 

1932 # Construct new text. 

1933 text = [] 

1934 p = 0 

1935 

1936 for p2 in buff.multiple_cursor_positions: 

1937 text.append(original_text[p:p2]) 

1938 text.append(event.data) 

1939 p = p2 

1940 

1941 text.append(original_text[p:]) 

1942 

1943 # Shift all cursor positions. 

1944 new_cursor_positions = [ 

1945 pos + i + 1 for i, pos in enumerate(buff.multiple_cursor_positions) 

1946 ] 

1947 

1948 # Set result. 

1949 buff.text = "".join(text) 

1950 buff.multiple_cursor_positions = new_cursor_positions 

1951 buff.cursor_position += 1 

1952 

1953 @handle("backspace", filter=vi_insert_multiple_mode) 

1954 def _delete_before_multiple_cursors(event: E) -> None: 

1955 """ 

1956 Backspace, using multiple cursors. 

1957 """ 

1958 buff = event.current_buffer 

1959 original_text = buff.text 

1960 

1961 # Construct new text. 

1962 deleted_something = False 

1963 text = [] 

1964 p = 0 

1965 

1966 for p2 in buff.multiple_cursor_positions: 

1967 if p2 > 0 and original_text[p2 - 1] != "\n": # Don't delete across lines. 

1968 text.append(original_text[p : p2 - 1]) 

1969 deleted_something = True 

1970 else: 

1971 text.append(original_text[p:p2]) 

1972 p = p2 

1973 

1974 text.append(original_text[p:]) 

1975 

1976 if deleted_something: 

1977 # Shift all cursor positions. 

1978 lengths = [len(part) for part in text[:-1]] 

1979 new_cursor_positions = list(accumulate(lengths)) 

1980 

1981 # Set result. 

1982 buff.text = "".join(text) 

1983 buff.multiple_cursor_positions = new_cursor_positions 

1984 buff.cursor_position -= 1 

1985 else: 

1986 event.app.output.bell() 

1987 

1988 @handle("delete", filter=vi_insert_multiple_mode) 

1989 def _delete_after_multiple_cursors(event: E) -> None: 

1990 """ 

1991 Delete, using multiple cursors. 

1992 """ 

1993 buff = event.current_buffer 

1994 original_text = buff.text 

1995 

1996 # Construct new text. 

1997 deleted_something = False 

1998 text = [] 

1999 new_cursor_positions = [] 

2000 p = 0 

2001 

2002 for p2 in buff.multiple_cursor_positions: 

2003 text.append(original_text[p:p2]) 

2004 if p2 >= len(original_text) or original_text[p2] == "\n": 

2005 # Don't delete across lines. 

2006 p = p2 

2007 else: 

2008 p = p2 + 1 

2009 deleted_something = True 

2010 

2011 text.append(original_text[p:]) 

2012 

2013 if deleted_something: 

2014 # Shift all cursor positions. 

2015 lengths = [len(part) for part in text[:-1]] 

2016 new_cursor_positions = list(accumulate(lengths)) 

2017 

2018 # Set result. 

2019 buff.text = "".join(text) 

2020 buff.multiple_cursor_positions = new_cursor_positions 

2021 else: 

2022 event.app.output.bell() 

2023 

2024 @handle("left", filter=vi_insert_multiple_mode) 

2025 def _left_multiple(event: E) -> None: 

2026 """ 

2027 Move all cursors to the left. 

2028 (But keep all cursors on the same line.) 

2029 """ 

2030 buff = event.current_buffer 

2031 new_positions = [] 

2032 

2033 for p in buff.multiple_cursor_positions: 

2034 if buff.document.translate_index_to_position(p)[1] > 0: 

2035 p -= 1 

2036 new_positions.append(p) 

2037 

2038 buff.multiple_cursor_positions = new_positions 

2039 

2040 if buff.document.cursor_position_col > 0: 

2041 buff.cursor_position -= 1 

2042 

2043 @handle("right", filter=vi_insert_multiple_mode) 

2044 def _right_multiple(event: E) -> None: 

2045 """ 

2046 Move all cursors to the right. 

2047 (But keep all cursors on the same line.) 

2048 """ 

2049 buff = event.current_buffer 

2050 new_positions = [] 

2051 

2052 for p in buff.multiple_cursor_positions: 

2053 row, column = buff.document.translate_index_to_position(p) 

2054 if column < len(buff.document.lines[row]): 

2055 p += 1 

2056 new_positions.append(p) 

2057 

2058 buff.multiple_cursor_positions = new_positions 

2059 

2060 if not buff.document.is_cursor_at_the_end_of_line: 

2061 buff.cursor_position += 1 

2062 

2063 @handle("up", filter=vi_insert_multiple_mode) 

2064 @handle("down", filter=vi_insert_multiple_mode) 

2065 def _updown_multiple(event: E) -> None: 

2066 """ 

2067 Ignore all up/down key presses when in multiple cursor mode. 

2068 """ 

2069 

2070 @handle("c-x", "c-l", filter=vi_insert_mode) 

2071 def _complete_line(event: E) -> None: 

2072 """ 

2073 Pressing the ControlX - ControlL sequence in Vi mode does line 

2074 completion based on the other lines in the document and the history. 

2075 """ 

2076 event.current_buffer.start_history_lines_completion() 

2077 

2078 @handle("c-x", "c-f", filter=vi_insert_mode) 

2079 def _complete_filename(event: E) -> None: 

2080 """ 

2081 Complete file names. 

2082 """ 

2083 # TODO 

2084 pass 

2085 

2086 @handle("c-k", filter=vi_insert_mode | vi_replace_mode) 

2087 def _digraph(event: E) -> None: 

2088 """ 

2089 Go into digraph mode. 

2090 """ 

2091 event.app.vi_state.waiting_for_digraph = True 

2092 

2093 @handle(Keys.Any, filter=vi_digraph_mode & ~digraph_symbol_1_given) 

2094 def _digraph1(event: E) -> None: 

2095 """ 

2096 First digraph symbol. 

2097 """ 

2098 event.app.vi_state.digraph_symbol1 = event.data 

2099 

2100 @handle(Keys.Any, filter=vi_digraph_mode & digraph_symbol_1_given) 

2101 def _create_digraph(event: E) -> None: 

2102 """ 

2103 Insert digraph. 

2104 """ 

2105 try: 

2106 # Lookup. 

2107 code: tuple[str, str] = ( 

2108 event.app.vi_state.digraph_symbol1 or "", 

2109 event.data, 

2110 ) 

2111 if code not in DIGRAPHS: 

2112 code = code[::-1] # Try reversing. 

2113 symbol = DIGRAPHS[code] 

2114 except KeyError: 

2115 # Unknown digraph. 

2116 event.app.output.bell() 

2117 else: 

2118 # Insert digraph. 

2119 overwrite = event.app.vi_state.input_mode == InputMode.REPLACE 

2120 event.current_buffer.insert_text(chr(symbol), overwrite=overwrite) 

2121 event.app.vi_state.waiting_for_digraph = False 

2122 finally: 

2123 event.app.vi_state.waiting_for_digraph = False 

2124 event.app.vi_state.digraph_symbol1 = None 

2125 

2126 @handle("c-o", filter=vi_insert_mode | vi_replace_mode) 

2127 def _quick_normal_mode(event: E) -> None: 

2128 """ 

2129 Go into normal mode for one single action. 

2130 """ 

2131 event.app.vi_state.temporary_navigation_mode = True 

2132 

2133 @handle("q", Keys.Any, filter=vi_navigation_mode & ~vi_recording_macro) 

2134 def _start_macro(event: E) -> None: 

2135 """ 

2136 Start recording macro. 

2137 """ 

2138 c = event.key_sequence[1].data 

2139 if c in vi_register_names: 

2140 vi_state = event.app.vi_state 

2141 

2142 vi_state.recording_register = c 

2143 vi_state.current_recording = "" 

2144 

2145 @handle("q", filter=vi_navigation_mode & vi_recording_macro) 

2146 def _stop_macro(event: E) -> None: 

2147 """ 

2148 Stop recording macro. 

2149 """ 

2150 vi_state = event.app.vi_state 

2151 

2152 # Store and stop recording. 

2153 if vi_state.recording_register: 

2154 vi_state.named_registers[vi_state.recording_register] = ClipboardData( 

2155 vi_state.current_recording 

2156 ) 

2157 vi_state.recording_register = None 

2158 vi_state.current_recording = "" 

2159 

2160 @handle("@", Keys.Any, filter=vi_navigation_mode, record_in_macro=False) 

2161 def _execute_macro(event: E) -> None: 

2162 """ 

2163 Execute macro. 

2164 

2165 Notice that we pass `record_in_macro=False`. This ensures that the `@x` 

2166 keys don't appear in the recording itself. This function inserts the 

2167 body of the called macro back into the KeyProcessor, so these keys will 

2168 be added later on to the macro of their handlers have 

2169 `record_in_macro=True`. 

2170 """ 

2171 # Retrieve macro. 

2172 c = event.key_sequence[1].data 

2173 try: 

2174 macro = event.app.vi_state.named_registers[c] 

2175 except KeyError: 

2176 return 

2177 

2178 # Expand macro (which is a string in the register), in individual keys. 

2179 # Use vt100 parser for this. 

2180 keys: list[KeyPress] = [] 

2181 

2182 parser = Vt100Parser(keys.append) 

2183 parser.feed(macro.text) 

2184 parser.flush() 

2185 

2186 # Now feed keys back to the input processor. 

2187 for _ in range(event.arg): 

2188 event.app.key_processor.feed_multiple(keys, first=True) 

2189 

2190 return ConditionalKeyBindings(key_bindings, vi_mode) 

2191 

2192 

2193def load_vi_search_bindings() -> KeyBindingsBase: 

2194 key_bindings = KeyBindings() 

2195 handle = key_bindings.add 

2196 from . import search 

2197 

2198 # Vi-style forward search. 

2199 handle( 

2200 "/", 

2201 filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed, 

2202 )(search.start_forward_incremental_search) 

2203 handle( 

2204 "?", 

2205 filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, 

2206 )(search.start_forward_incremental_search) 

2207 handle("c-s")(search.start_forward_incremental_search) 

2208 

2209 # Vi-style backward search. 

2210 handle( 

2211 "?", 

2212 filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed, 

2213 )(search.start_reverse_incremental_search) 

2214 handle( 

2215 "/", 

2216 filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, 

2217 )(search.start_reverse_incremental_search) 

2218 handle("c-r")(search.start_reverse_incremental_search) 

2219 

2220 # Apply the search. (At the / or ? prompt.) 

2221 handle("enter", filter=is_searching)(search.accept_search) 

2222 

2223 handle("c-r", filter=is_searching)(search.reverse_incremental_search) 

2224 handle("c-s", filter=is_searching)(search.forward_incremental_search) 

2225 

2226 handle("c-c")(search.abort_search) 

2227 handle("c-g")(search.abort_search) 

2228 handle("backspace", filter=search_buffer_is_empty)(search.abort_search) 

2229 

2230 # Handle escape. This should accept the search, just like readline. 

2231 # `abort_search` would be a meaningful alternative. 

2232 handle("escape")(search.accept_search) 

2233 

2234 return ConditionalKeyBindings(key_bindings, vi_mode)