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

956 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 06:09 +0000

1# pylint: disable=function-redefined 

2from __future__ import annotations 

3 

4import codecs 

5import string 

6from enum import Enum 

7from itertools import accumulate 

8from typing import Callable, Iterable, Tuple, TypeVar 

9 

10from prompt_toolkit.application.current import get_app 

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

12from prompt_toolkit.clipboard import ClipboardData 

13from prompt_toolkit.document import Document 

14from prompt_toolkit.filters import ( 

15 Always, 

16 Condition, 

17 Filter, 

18 has_arg, 

19 is_read_only, 

20 is_searching, 

21) 

22from prompt_toolkit.filters.app import ( 

23 in_paste_mode, 

24 is_multiline, 

25 vi_digraph_mode, 

26 vi_insert_mode, 

27 vi_insert_multiple_mode, 

28 vi_mode, 

29 vi_navigation_mode, 

30 vi_recording_macro, 

31 vi_replace_mode, 

32 vi_replace_single_mode, 

33 vi_search_direction_reversed, 

34 vi_selection_mode, 

35 vi_waiting_for_text_object_mode, 

36) 

37from prompt_toolkit.input.vt100_parser import Vt100Parser 

38from prompt_toolkit.key_binding.digraphs import DIGRAPHS 

39from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent 

40from prompt_toolkit.key_binding.vi_state import CharacterFind, InputMode 

41from prompt_toolkit.keys import Keys 

42from prompt_toolkit.search import SearchDirection 

43from prompt_toolkit.selection import PasteMode, SelectionState, SelectionType 

44 

45from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase 

46from .named_commands import get_by_name 

47 

48__all__ = [ 

49 "load_vi_bindings", 

50 "load_vi_search_bindings", 

51] 

52 

53E = KeyPressEvent 

54 

55ascii_lowercase = string.ascii_lowercase 

56 

57vi_register_names = ascii_lowercase + "0123456789" 

58 

59 

60class TextObjectType(Enum): 

61 EXCLUSIVE = "EXCLUSIVE" 

62 INCLUSIVE = "INCLUSIVE" 

63 LINEWISE = "LINEWISE" 

64 BLOCK = "BLOCK" 

65 

66 

67class TextObject: 

68 """ 

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

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

71 """ 

72 

73 def __init__( 

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

75 ): 

76 self.start = start 

77 self.end = end 

78 self.type = type 

79 

80 @property 

81 def selection_type(self) -> SelectionType: 

82 if self.type == TextObjectType.LINEWISE: 

83 return SelectionType.LINES 

84 if self.type == TextObjectType.BLOCK: 

85 return SelectionType.BLOCK 

86 else: 

87 return SelectionType.CHARACTERS 

88 

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

90 """ 

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

92 """ 

93 if self.start < self.end: 

94 return self.start, self.end 

95 else: 

96 return self.end, self.start 

97 

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

99 """ 

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

101 operators should operate on. 

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

103 

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

105 position is *not* included. 

106 """ 

107 start, end = self.sorted() 

108 doc = document 

109 

110 if ( 

111 self.type == TextObjectType.EXCLUSIVE 

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

113 ): 

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

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

116 end -= 1 

117 if self.type == TextObjectType.INCLUSIVE: 

118 end += 1 

119 if self.type == TextObjectType.LINEWISE: 

120 # Select whole lines 

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

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

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

124 end = ( 

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

126 - doc.cursor_position 

127 ) 

128 return start, end 

129 

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

131 """ 

132 Return a (start_line, end_line) pair. 

133 """ 

134 # Get absolute cursor positions from the text object. 

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

136 from_ += buffer.cursor_position 

137 to += buffer.cursor_position 

138 

139 # Take the start of the lines. 

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

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

142 

143 return from_, to 

144 

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

146 """ 

147 Turn text object into `ClipboardData` instance. 

148 """ 

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

150 

151 from_ += buffer.cursor_position 

152 to += buffer.cursor_position 

153 

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

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

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

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

158 if self.type != TextObjectType.LINEWISE: 

159 to -= 1 

160 

161 document = Document( 

162 buffer.text, 

163 to, 

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

165 ) 

166 

167 new_document, clipboard_data = document.cut_selection() 

168 return new_document, clipboard_data 

169 

170 

171# Typevar for any text object function: 

172TextObjectFunction = Callable[[E], TextObject] 

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

174 

175 

176def create_text_object_decorator( 

177 key_bindings: KeyBindings, 

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

179 """ 

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

181 """ 

182 

183 def text_object_decorator( 

184 *keys: Keys | str, 

185 filter: Filter = Always(), 

186 no_move_handler: bool = False, 

187 no_selection_handler: bool = False, 

188 eager: bool = False, 

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

190 """ 

191 Register a text object function. 

192 

193 Usage:: 

194 

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

196 def handler(event): 

197 # Return a text object for this key. 

198 return TextObject(...) 

199 

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

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

202 """ 

203 

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

205 @key_bindings.add( 

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

207 ) 

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

209 # Arguments are multiplied. 

210 vi_state = event.app.vi_state 

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

212 

213 # Call the text object handler. 

214 text_obj = text_object_func(event) 

215 

216 # Get the operator function. 

217 # (Should never be None here, given the 

218 # `vi_waiting_for_text_object_mode` filter state.) 

219 operator_func = vi_state.operator_func 

220 

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

222 # Call the operator function with the text object. 

223 operator_func(event, text_obj) 

224 

225 # Clear operator. 

226 event.app.vi_state.operator_func = None 

227 event.app.vi_state.operator_arg = None 

228 

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

230 if not no_move_handler: 

231 

232 @key_bindings.add( 

233 *keys, 

234 filter=~vi_waiting_for_text_object_mode 

235 & filter 

236 & vi_navigation_mode, 

237 eager=eager, 

238 ) 

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

240 """ 

241 Move handler for navigation mode. 

242 """ 

243 text_object = text_object_func(event) 

244 event.current_buffer.cursor_position += text_object.start 

245 

246 # Register a move selection operation. 

247 if not no_selection_handler: 

248 

249 @key_bindings.add( 

250 *keys, 

251 filter=~vi_waiting_for_text_object_mode 

252 & filter 

253 & vi_selection_mode, 

254 eager=eager, 

255 ) 

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

257 """ 

258 Move handler for selection mode. 

259 """ 

260 text_object = text_object_func(event) 

261 buff = event.current_buffer 

262 selection_state = buff.selection_state 

263 

264 if selection_state is None: 

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

266 

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

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

269 if text_object.end: 

270 # Take selection positions from text object. 

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

272 start += buff.cursor_position 

273 end += buff.cursor_position 

274 

275 selection_state.original_cursor_position = start 

276 buff.cursor_position = end 

277 

278 # Take selection type from text object. 

279 if text_object.type == TextObjectType.LINEWISE: 

280 selection_state.type = SelectionType.LINES 

281 else: 

282 selection_state.type = SelectionType.CHARACTERS 

283 else: 

284 event.current_buffer.cursor_position += text_object.start 

285 

286 # Make it possible to chain @text_object decorators. 

287 return text_object_func 

288 

289 return decorator 

290 

291 return text_object_decorator 

292 

293 

294# Typevar for any operator function: 

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

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

297 

298 

299def create_operator_decorator( 

300 key_bindings: KeyBindings, 

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

302 """ 

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

304 """ 

305 

306 def operator_decorator( 

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

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

309 """ 

310 Register a Vi operator. 

311 

312 Usage:: 

313 

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

315 def handler(event, text_object): 

316 # Do something with the text object here. 

317 """ 

318 

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

320 @key_bindings.add( 

321 *keys, 

322 filter=~vi_waiting_for_text_object_mode & filter & vi_navigation_mode, 

323 eager=eager, 

324 ) 

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

326 """ 

327 Handle operator in navigation mode. 

328 """ 

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

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

331 # object has been received. 

332 event.app.vi_state.operator_func = operator_func 

333 event.app.vi_state.operator_arg = event.arg 

334 

335 @key_bindings.add( 

336 *keys, 

337 filter=~vi_waiting_for_text_object_mode & filter & vi_selection_mode, 

338 eager=eager, 

339 ) 

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

341 """ 

342 Handle operator in selection mode. 

343 """ 

344 buff = event.current_buffer 

345 selection_state = buff.selection_state 

346 

347 if selection_state is not None: 

348 # Create text object from selection. 

349 if selection_state.type == SelectionType.LINES: 

350 text_obj_type = TextObjectType.LINEWISE 

351 elif selection_state.type == SelectionType.BLOCK: 

352 text_obj_type = TextObjectType.BLOCK 

353 else: 

354 text_obj_type = TextObjectType.INCLUSIVE 

355 

356 text_object = TextObject( 

357 selection_state.original_cursor_position - buff.cursor_position, 

358 type=text_obj_type, 

359 ) 

360 

361 # Execute operator. 

362 operator_func(event, text_object) 

363 

364 # Quit selection mode. 

365 buff.selection_state = None 

366 

367 return operator_func 

368 

369 return decorator 

370 

371 return operator_decorator 

372 

373 

374def load_vi_bindings() -> KeyBindingsBase: 

375 """ 

376 Vi extensions. 

377 

378 # Overview of Readline Vi commands: 

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

380 """ 

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

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

383 # read-only buffer. 

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

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

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

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

388 # bindings that do text manipulation. 

389 

390 key_bindings = KeyBindings() 

391 handle = key_bindings.add 

392 

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

394 # ViState says different.) 

395 

396 TransformFunction = Tuple[Tuple[str, ...], Filter, Callable[[str], str]] 

397 

398 vi_transform_functions: list[TransformFunction] = [ 

399 # Rot 13 transformation 

400 ( 

401 ("g", "?"), 

402 Always(), 

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

404 ), 

405 # To lowercase 

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

407 # To uppercase. 

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

409 # Swap case. 

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

411 ( 

412 ("~",), 

413 Condition(lambda: get_app().vi_state.tilde_operator), 

414 lambda string: string.swapcase(), 

415 ), 

416 ] 

417 

418 # Insert a character literally (quoted insert). 

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

420 

421 @handle("escape") 

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

423 """ 

424 Escape goes to vi navigation mode. 

425 """ 

426 buffer = event.current_buffer 

427 vi_state = event.app.vi_state 

428 

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

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

431 

432 vi_state.input_mode = InputMode.NAVIGATION 

433 

434 if bool(buffer.selection_state): 

435 buffer.exit_selection() 

436 

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

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

439 """ 

440 Arrow up in selection mode. 

441 """ 

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

443 

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

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

446 """ 

447 Arrow down in selection mode. 

448 """ 

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

450 

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

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

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

454 """ 

455 Arrow up and ControlP in navigation mode go up. 

456 """ 

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

458 

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

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

461 """ 

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

463 line. 

464 """ 

465 event.current_buffer.auto_up( 

466 count=event.arg, go_to_start_of_line_if_history_changes=True 

467 ) 

468 

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

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

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

472 """ 

473 Arrow down and Control-N in navigation mode. 

474 """ 

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

476 

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

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

479 """ 

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

481 """ 

482 event.current_buffer.auto_down( 

483 count=event.arg, go_to_start_of_line_if_history_changes=True 

484 ) 

485 

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

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

488 """ 

489 In navigation-mode, move cursor. 

490 """ 

491 event.current_buffer.cursor_position += ( 

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

493 ) 

494 

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

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

497 b = event.current_buffer 

498 

499 if b.complete_state: 

500 b.complete_next() 

501 else: 

502 b.start_completion(select_first=True) 

503 

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

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

506 """ 

507 Control-P: To previous completion. 

508 """ 

509 b = event.current_buffer 

510 

511 if b.complete_state: 

512 b.complete_previous() 

513 else: 

514 b.start_completion(select_last=True) 

515 

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

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

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

519 """ 

520 Accept current completion. 

521 """ 

522 event.current_buffer.complete_state = None 

523 

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

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

526 """ 

527 Cancel completion. Go back to originally typed text. 

528 """ 

529 event.current_buffer.cancel_completion() 

530 

531 @Condition 

532 def is_returnable() -> bool: 

533 return get_app().current_buffer.is_returnable 

534 

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

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

537 get_by_name("accept-line") 

538 ) 

539 

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

541 # has been marked as single line. 

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

543 

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

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

546 """ 

547 Go to the beginning of next line. 

548 """ 

549 b = event.current_buffer 

550 b.cursor_down(count=event.arg) 

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

552 after_whitespace=True 

553 ) 

554 

555 # ** In navigation mode ** 

556 

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

558 

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

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

561 """ 

562 Pressing the Insert key. 

563 """ 

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

565 

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

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

568 """ 

569 Pressing the Insert key. 

570 """ 

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

572 

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

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

575 # read-only buffers. 

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

577 event.current_buffer.cursor_position += ( 

578 event.current_buffer.document.get_cursor_right_position() 

579 ) 

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

581 

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

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

584 event.current_buffer.cursor_position += ( 

585 event.current_buffer.document.get_end_of_line_position() 

586 ) 

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

588 

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

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

591 """ 

592 Change to end of line. 

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

594 """ 

595 buffer = event.current_buffer 

596 

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

598 event.app.clipboard.set_text(deleted) 

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

600 

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

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

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

604 """ 

605 Change current line 

606 """ 

607 buffer = event.current_buffer 

608 

609 # We copy the whole line. 

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

611 event.app.clipboard.set_data(data) 

612 

613 # But we delete after the whitespace 

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

615 after_whitespace=True 

616 ) 

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

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

619 

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

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

622 """ 

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

624 """ 

625 buffer = event.current_buffer 

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

627 event.app.clipboard.set_text(deleted) 

628 

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

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

631 """ 

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

633 """ 

634 buffer = event.current_buffer 

635 

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

637 lines = buffer.document.lines 

638 

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

640 deleted = "\n".join( 

641 lines[ 

642 buffer.document.cursor_position_row : buffer.document.cursor_position_row 

643 + event.arg 

644 ] 

645 ) 

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

647 

648 # Set new text. 

649 if before and after: 

650 before = before + "\n" 

651 

652 # Set text and cursor position. 

653 buffer.document = Document( 

654 text=before + after, 

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

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

657 ) 

658 

659 # Set clipboard data 

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

661 

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

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

664 """ 

665 Cut selection. 

666 ('x' is not an operator.) 

667 """ 

668 clipboard_data = event.current_buffer.cut_selection() 

669 event.app.clipboard.set_data(clipboard_data) 

670 

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

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

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

674 

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

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

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

678 event.current_buffer.cursor_position += ( 

679 event.current_buffer.document.get_start_of_line_position( 

680 after_whitespace=True 

681 ) 

682 ) 

683 

684 @Condition 

685 def in_block_selection() -> bool: 

686 buff = get_app().current_buffer 

687 return bool( 

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

689 ) 

690 

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

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

693 """ 

694 Insert in block selection mode. 

695 """ 

696 buff = event.current_buffer 

697 

698 # Store all cursor positions. 

699 positions = [] 

700 

701 if after: 

702 

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

704 return from_to[1] 

705 

706 else: 

707 

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

709 return from_to[0] 

710 

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

712 positions.append(get_pos(from_to)) 

713 if i == 0: 

714 buff.cursor_position = get_pos(from_to) 

715 

716 buff.multiple_cursor_positions = positions 

717 

718 # Go to 'INSERT_MULTIPLE' mode. 

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

720 buff.exit_selection() 

721 

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

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

724 insert_in_block_selection(event, after=True) 

725 

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

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

728 """ 

729 Join lines. 

730 """ 

731 for i in range(event.arg): 

732 event.current_buffer.join_next_line() 

733 

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

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

736 """ 

737 Join lines without space. 

738 """ 

739 for i in range(event.arg): 

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

741 

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

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

744 """ 

745 Join selected lines. 

746 """ 

747 event.current_buffer.join_selected_lines() 

748 

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

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

751 """ 

752 Join selected lines without space. 

753 """ 

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

755 

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

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

758 """ 

759 Paste after 

760 """ 

761 event.current_buffer.paste_clipboard_data( 

762 event.app.clipboard.get_data(), 

763 count=event.arg, 

764 paste_mode=PasteMode.VI_AFTER, 

765 ) 

766 

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

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

769 """ 

770 Paste before 

771 """ 

772 event.current_buffer.paste_clipboard_data( 

773 event.app.clipboard.get_data(), 

774 count=event.arg, 

775 paste_mode=PasteMode.VI_BEFORE, 

776 ) 

777 

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

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

780 """ 

781 Paste from named register. 

782 """ 

783 c = event.key_sequence[1].data 

784 if c in vi_register_names: 

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

786 if data: 

787 event.current_buffer.paste_clipboard_data( 

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

789 ) 

790 

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

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

793 """ 

794 Paste (before) from named register. 

795 """ 

796 c = event.key_sequence[1].data 

797 if c in vi_register_names: 

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

799 if data: 

800 event.current_buffer.paste_clipboard_data( 

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

802 ) 

803 

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

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

806 """ 

807 Go to 'replace-single'-mode. 

808 """ 

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

810 

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

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

813 """ 

814 Go to 'replace'-mode. 

815 """ 

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

817 

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

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

820 """ 

821 Substitute with new text 

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

823 """ 

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

825 event.app.clipboard.set_text(text) 

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

827 

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

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

830 for i in range(event.arg): 

831 event.current_buffer.undo() 

832 

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

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

835 """ 

836 Start lines selection. 

837 """ 

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

839 

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

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

842 """ 

843 Enter block selection mode. 

844 """ 

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

846 

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

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

849 """ 

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

851 selection mode. 

852 """ 

853 selection_state = event.current_buffer.selection_state 

854 

855 if selection_state is not None: 

856 if selection_state.type != SelectionType.LINES: 

857 selection_state.type = SelectionType.LINES 

858 else: 

859 event.current_buffer.exit_selection() 

860 

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

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

863 """ 

864 Enter character selection mode. 

865 """ 

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

867 

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

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

870 """ 

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

872 to character selection mode. 

873 """ 

874 selection_state = event.current_buffer.selection_state 

875 

876 if selection_state is not None: 

877 if selection_state.type != SelectionType.CHARACTERS: 

878 selection_state.type = SelectionType.CHARACTERS 

879 else: 

880 event.current_buffer.exit_selection() 

881 

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

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

884 """ 

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

886 selection mode. 

887 """ 

888 selection_state = event.current_buffer.selection_state 

889 

890 if selection_state is not None: 

891 if selection_state.type != SelectionType.BLOCK: 

892 selection_state.type = SelectionType.BLOCK 

893 else: 

894 event.current_buffer.exit_selection() 

895 

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

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

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

899 """ 

900 Switch from visual linewise mode to visual characterwise mode. 

901 """ 

902 buffer = event.current_buffer 

903 

904 if ( 

905 buffer.selection_state 

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

907 ): 

908 buffer.selection_state.type = SelectionType.CHARACTERS 

909 

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

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

912 """ 

913 Delete character. 

914 """ 

915 buff = event.current_buffer 

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

917 if count: 

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

919 event.app.clipboard.set_text(text) 

920 

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

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

923 buff = event.current_buffer 

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

925 if count: 

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

927 event.app.clipboard.set_text(text) 

928 

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

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

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

932 """ 

933 Yank the whole line. 

934 """ 

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

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

937 

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

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

940 """ 

941 Move to first non whitespace of next line 

942 """ 

943 buffer = event.current_buffer 

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

945 count=event.arg 

946 ) 

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

948 after_whitespace=True 

949 ) 

950 

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

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

953 """ 

954 Move to first non whitespace of previous line 

955 """ 

956 buffer = event.current_buffer 

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

958 count=event.arg 

959 ) 

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

961 after_whitespace=True 

962 ) 

963 

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

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

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

967 """ 

968 Indent lines. 

969 """ 

970 buffer = event.current_buffer 

971 current_row = buffer.document.cursor_position_row 

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

973 

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

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

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

977 """ 

978 Unindent lines. 

979 """ 

980 current_row = event.current_buffer.document.cursor_position_row 

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

982 

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

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

985 """ 

986 Open line above and enter insertion mode 

987 """ 

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

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

990 

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

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

993 """ 

994 Open line below and enter insertion mode 

995 """ 

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

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

998 

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

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

1001 """ 

1002 Reverse case of current character and move cursor forward. 

1003 """ 

1004 buffer = event.current_buffer 

1005 c = buffer.document.current_char 

1006 

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

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

1009 

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

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

1012 """ 

1013 Lowercase current line. 

1014 """ 

1015 buff = event.current_buffer 

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

1017 

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

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

1020 """ 

1021 Uppercase current line. 

1022 """ 

1023 buff = event.current_buffer 

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

1025 

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

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

1028 """ 

1029 Swap case of the current line. 

1030 """ 

1031 buff = event.current_buffer 

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

1033 

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

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

1036 """ 

1037 Go to previous occurrence of this word. 

1038 """ 

1039 b = event.current_buffer 

1040 search_state = event.app.current_search_state 

1041 

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

1043 search_state.direction = SearchDirection.BACKWARD 

1044 

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

1046 

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

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

1049 """ 

1050 Go to next occurrence of this word. 

1051 """ 

1052 b = event.current_buffer 

1053 search_state = event.app.current_search_state 

1054 

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

1056 search_state.direction = SearchDirection.FORWARD 

1057 

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

1059 

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

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

1062 # TODO: go to begin of sentence. 

1063 # XXX: should become text_object. 

1064 pass 

1065 

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

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

1068 # TODO: go to end of sentence. 

1069 # XXX: should become text_object. 

1070 pass 

1071 

1072 operator = create_operator_decorator(key_bindings) 

1073 text_object = create_text_object_decorator(key_bindings) 

1074 

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

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

1077 """ 

1078 Unknown key binding while waiting for a text object. 

1079 """ 

1080 event.app.output.bell() 

1081 

1082 # 

1083 # *** Operators *** 

1084 # 

1085 

1086 def create_delete_and_change_operators( 

1087 delete_only: bool, with_register: bool = False 

1088 ) -> None: 

1089 """ 

1090 Delete and change operators. 

1091 

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

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

1094 """ 

1095 handler_keys: Iterable[str] 

1096 if with_register: 

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

1098 else: 

1099 handler_keys = "cd"[delete_only] 

1100 

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

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

1103 clipboard_data = None 

1104 buff = event.current_buffer 

1105 

1106 if text_object: 

1107 new_document, clipboard_data = text_object.cut(buff) 

1108 buff.document = new_document 

1109 

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

1111 if clipboard_data and clipboard_data.text: 

1112 if with_register: 

1113 reg_name = event.key_sequence[1].data 

1114 if reg_name in vi_register_names: 

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

1116 else: 

1117 event.app.clipboard.set_data(clipboard_data) 

1118 

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

1120 if not delete_only: 

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

1122 

1123 create_delete_and_change_operators(False, False) 

1124 create_delete_and_change_operators(False, True) 

1125 create_delete_and_change_operators(True, False) 

1126 create_delete_and_change_operators(True, True) 

1127 

1128 def create_transform_handler( 

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

1130 ) -> None: 

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

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

1133 """ 

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

1135 """ 

1136 buff = event.current_buffer 

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

1138 

1139 if start < end: 

1140 # Transform. 

1141 buff.transform_region( 

1142 buff.cursor_position + start, 

1143 buff.cursor_position + end, 

1144 transform_func, 

1145 ) 

1146 

1147 # Move cursor 

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

1149 

1150 for k, f, func in vi_transform_functions: 

1151 create_transform_handler(f, func, *k) 

1152 

1153 @operator("y") 

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

1155 """ 

1156 Yank operator. (Copy text.) 

1157 """ 

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

1159 if clipboard_data.text: 

1160 event.app.clipboard.set_data(clipboard_data) 

1161 

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

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

1164 """ 

1165 Yank selection to named register. 

1166 """ 

1167 c = event.key_sequence[1].data 

1168 if c in vi_register_names: 

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

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

1171 

1172 @operator(">") 

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

1174 """ 

1175 Indent. 

1176 """ 

1177 buff = event.current_buffer 

1178 from_, to = text_object.get_line_numbers(buff) 

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

1180 

1181 @operator("<") 

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

1183 """ 

1184 Unindent. 

1185 """ 

1186 buff = event.current_buffer 

1187 from_, to = text_object.get_line_numbers(buff) 

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

1189 

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

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

1192 """ 

1193 Reshape text. 

1194 """ 

1195 buff = event.current_buffer 

1196 from_, to = text_object.get_line_numbers(buff) 

1197 reshape_text(buff, from_, to) 

1198 

1199 # 

1200 # *** Text objects *** 

1201 # 

1202 

1203 @text_object("b") 

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

1205 """ 

1206 Move one word or token left. 

1207 """ 

1208 return TextObject( 

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

1210 or 0 

1211 ) 

1212 

1213 @text_object("B") 

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

1215 """ 

1216 Move one non-blank word left 

1217 """ 

1218 return TextObject( 

1219 event.current_buffer.document.find_start_of_previous_word( 

1220 count=event.arg, WORD=True 

1221 ) 

1222 or 0 

1223 ) 

1224 

1225 @text_object("$") 

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

1227 """ 

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

1229 """ 

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

1231 

1232 @text_object("w") 

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

1234 """ 

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

1236 """ 

1237 return TextObject( 

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

1239 or event.current_buffer.document.get_end_of_document_position() 

1240 ) 

1241 

1242 @text_object("W") 

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

1244 """ 

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

1246 """ 

1247 return TextObject( 

1248 event.current_buffer.document.find_next_word_beginning( 

1249 count=event.arg, WORD=True 

1250 ) 

1251 or event.current_buffer.document.get_end_of_document_position() 

1252 ) 

1253 

1254 @text_object("e") 

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

1256 """ 

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

1258 """ 

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

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

1261 

1262 @text_object("E") 

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

1264 """ 

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

1266 """ 

1267 end = event.current_buffer.document.find_next_word_ending( 

1268 count=event.arg, WORD=True 

1269 ) 

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

1271 

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

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

1274 """ 

1275 Inner 'word': ciw and diw 

1276 """ 

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

1278 return TextObject(start, end) 

1279 

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

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

1282 """ 

1283 A 'word': caw and daw 

1284 """ 

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

1286 include_trailing_whitespace=True 

1287 ) 

1288 return TextObject(start, end) 

1289 

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

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

1292 """ 

1293 Inner 'WORD': ciW and diW 

1294 """ 

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

1296 WORD=True 

1297 ) 

1298 return TextObject(start, end) 

1299 

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

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

1302 """ 

1303 A 'WORD': caw and daw 

1304 """ 

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

1306 WORD=True, include_trailing_whitespace=True 

1307 ) 

1308 return TextObject(start, end) 

1309 

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

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

1312 """ 

1313 Auto paragraph. 

1314 """ 

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

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

1317 return TextObject(start, end) 

1318 

1319 @text_object("^") 

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

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

1322 return TextObject( 

1323 event.current_buffer.document.get_start_of_line_position( 

1324 after_whitespace=True 

1325 ) 

1326 ) 

1327 

1328 @text_object("0") 

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

1330 """ 

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

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

1333 """ 

1334 return TextObject( 

1335 event.current_buffer.document.get_start_of_line_position( 

1336 after_whitespace=False 

1337 ) 

1338 ) 

1339 

1340 def create_ci_ca_handles( 

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

1342 ) -> None: 

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

1344 """ 

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

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

1347 """ 

1348 

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

1350 if ci_start == ci_end: 

1351 # Quotes 

1352 start = event.current_buffer.document.find_backwards( 

1353 ci_start, in_current_line=False 

1354 ) 

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

1356 else: 

1357 # Brackets 

1358 start = event.current_buffer.document.find_enclosing_bracket_left( 

1359 ci_start, ci_end 

1360 ) 

1361 end = event.current_buffer.document.find_enclosing_bracket_right( 

1362 ci_start, ci_end 

1363 ) 

1364 

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

1366 offset = 0 if inner else 1 

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

1368 else: 

1369 # Nothing found. 

1370 return TextObject(0) 

1371 

1372 if key is None: 

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

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

1375 else: 

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

1377 

1378 for inner in (False, True): 

1379 for ci_start, ci_end in [ 

1380 ('"', '"'), 

1381 ("'", "'"), 

1382 ("`", "`"), 

1383 ("[", "]"), 

1384 ("<", ">"), 

1385 ("{", "}"), 

1386 ("(", ")"), 

1387 ]: 

1388 create_ci_ca_handles(ci_start, ci_end, inner) 

1389 

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

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

1392 

1393 @text_object("{") 

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

1395 """ 

1396 Move to previous blank-line separated section. 

1397 Implements '{', 'c{', 'd{', 'y{' 

1398 """ 

1399 index = event.current_buffer.document.start_of_paragraph( 

1400 count=event.arg, before=True 

1401 ) 

1402 return TextObject(index) 

1403 

1404 @text_object("}") 

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

1406 """ 

1407 Move to next blank-line separated section. 

1408 Implements '}', 'c}', 'd}', 'y}' 

1409 """ 

1410 index = event.current_buffer.document.end_of_paragraph( 

1411 count=event.arg, after=True 

1412 ) 

1413 return TextObject(index) 

1414 

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

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

1417 """ 

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

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

1420 """ 

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

1422 match = event.current_buffer.document.find( 

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

1424 ) 

1425 if match: 

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

1427 else: 

1428 return TextObject(0) 

1429 

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

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

1432 """ 

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

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

1435 """ 

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

1437 return TextObject( 

1438 event.current_buffer.document.find_backwards( 

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

1440 ) 

1441 or 0 

1442 ) 

1443 

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

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

1446 """ 

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

1448 """ 

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

1450 match = event.current_buffer.document.find( 

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

1452 ) 

1453 if match: 

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

1455 else: 

1456 return TextObject(0) 

1457 

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

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

1460 """ 

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

1462 """ 

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

1464 match = event.current_buffer.document.find_backwards( 

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

1466 ) 

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

1468 

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

1470 """ 

1471 Create ',' and ';' commands. 

1472 """ 

1473 

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

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

1476 """ 

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

1478 """ 

1479 pos: int | None = 0 

1480 vi_state = event.app.vi_state 

1481 

1482 type = TextObjectType.EXCLUSIVE 

1483 

1484 if vi_state.last_character_find: 

1485 char = vi_state.last_character_find.character 

1486 backwards = vi_state.last_character_find.backwards 

1487 

1488 if reverse: 

1489 backwards = not backwards 

1490 

1491 if backwards: 

1492 pos = event.current_buffer.document.find_backwards( 

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

1494 ) 

1495 else: 

1496 pos = event.current_buffer.document.find( 

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

1498 ) 

1499 type = TextObjectType.INCLUSIVE 

1500 if pos: 

1501 return TextObject(pos, type=type) 

1502 else: 

1503 return TextObject(0) 

1504 

1505 repeat(True) 

1506 repeat(False) 

1507 

1508 @text_object("h") 

1509 @text_object("left") 

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

1511 """ 

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

1513 """ 

1514 return TextObject( 

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

1516 ) 

1517 

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

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

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

1521 # `buffer.preferred_column`. 

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

1523 """ 

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

1525 """ 

1526 return TextObject( 

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

1528 type=TextObjectType.LINEWISE, 

1529 ) 

1530 

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

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

1533 """ 

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

1535 """ 

1536 return TextObject( 

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

1538 type=TextObjectType.LINEWISE, 

1539 ) 

1540 

1541 @text_object("l") 

1542 @text_object(" ") 

1543 @text_object("right") 

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

1545 """ 

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

1547 """ 

1548 return TextObject( 

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

1550 ) 

1551 

1552 @text_object("H") 

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

1554 """ 

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

1556 Implements 'cH', 'dH', 'H'. 

1557 """ 

1558 w = event.app.layout.current_window 

1559 b = event.current_buffer 

1560 

1561 if w and w.render_info: 

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

1563 # move to the start of the visible area. 

1564 pos = ( 

1565 b.document.translate_row_col_to_index( 

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

1567 ) 

1568 - b.cursor_position 

1569 ) 

1570 

1571 else: 

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

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

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

1575 

1576 @text_object("M") 

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

1578 """ 

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

1580 Implements 'cM', 'dM', 'M'. 

1581 """ 

1582 w = event.app.layout.current_window 

1583 b = event.current_buffer 

1584 

1585 if w and w.render_info: 

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

1587 # move to the center of the visible area. 

1588 pos = ( 

1589 b.document.translate_row_col_to_index( 

1590 w.render_info.center_visible_line(), 0 

1591 ) 

1592 - b.cursor_position 

1593 ) 

1594 

1595 else: 

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

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

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

1599 

1600 @text_object("L") 

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

1602 """ 

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

1604 """ 

1605 w = event.app.layout.current_window 

1606 b = event.current_buffer 

1607 

1608 if w and w.render_info: 

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

1610 # move to the end of the visible area. 

1611 pos = ( 

1612 b.document.translate_row_col_to_index( 

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

1614 ) 

1615 - b.cursor_position 

1616 ) 

1617 

1618 else: 

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

1620 pos = len(b.document.text_after_cursor) 

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

1622 

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

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

1625 """ 

1626 Search next. 

1627 """ 

1628 buff = event.current_buffer 

1629 search_state = event.app.current_search_state 

1630 

1631 cursor_position = buff.get_search_position( 

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

1633 ) 

1634 return TextObject(cursor_position - buff.cursor_position) 

1635 

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

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

1638 """ 

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

1640 """ 

1641 search_state = event.app.current_search_state 

1642 

1643 event.current_buffer.apply_search( 

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

1645 ) 

1646 

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

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

1649 """ 

1650 Search previous. 

1651 """ 

1652 buff = event.current_buffer 

1653 search_state = event.app.current_search_state 

1654 

1655 cursor_position = buff.get_search_position( 

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

1657 ) 

1658 return TextObject(cursor_position - buff.cursor_position) 

1659 

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

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

1662 """ 

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

1664 """ 

1665 search_state = event.app.current_search_state 

1666 

1667 event.current_buffer.apply_search( 

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

1669 ) 

1670 

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

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

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

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

1675 """ 

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

1677 """ 

1678 b = event.current_buffer 

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

1680 

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

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

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

1684 """ 

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

1686 """ 

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

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

1689 # again. 

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

1691 

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

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

1694 """ 

1695 Center Window vertically around cursor. 

1696 """ 

1697 w = event.app.layout.current_window 

1698 b = event.current_buffer 

1699 

1700 if w and w.render_info: 

1701 info = w.render_info 

1702 

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

1704 # containing the cursor in the center. 

1705 scroll_height = info.window_height // 2 

1706 

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

1708 height = 0 

1709 while y > 0: 

1710 line_height = info.get_height_for_line(y) 

1711 

1712 if height + line_height < scroll_height: 

1713 height += line_height 

1714 y -= 1 

1715 else: 

1716 break 

1717 

1718 w.vertical_scroll = y 

1719 

1720 @text_object("%") 

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

1722 """ 

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

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

1725 """ 

1726 buffer = event.current_buffer 

1727 

1728 if event._arg: 

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

1730 # row in the file. 

1731 if 0 < event.arg <= 100: 

1732 absolute_index = buffer.document.translate_row_col_to_index( 

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

1734 ) 

1735 return TextObject( 

1736 absolute_index - buffer.document.cursor_position, 

1737 type=TextObjectType.LINEWISE, 

1738 ) 

1739 else: 

1740 return TextObject(0) # Do nothing. 

1741 

1742 else: 

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

1744 match = buffer.document.find_matching_bracket_position() 

1745 if match: 

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

1747 else: 

1748 return TextObject(0) 

1749 

1750 @text_object("|") 

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

1752 """ 

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

1754 number keys, for example, 20|). 

1755 """ 

1756 return TextObject( 

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

1758 ) 

1759 

1760 @text_object("g", "g") 

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

1762 """ 

1763 Go to the start of the very first line. 

1764 Implements 'gg', 'cgg', 'ygg' 

1765 """ 

1766 d = event.current_buffer.document 

1767 

1768 if event._arg: 

1769 # Move to the given line. 

1770 return TextObject( 

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

1772 type=TextObjectType.LINEWISE, 

1773 ) 

1774 else: 

1775 # Move to the top of the input. 

1776 return TextObject( 

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

1778 ) 

1779 

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

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

1782 """ 

1783 Go to last non-blank of line. 

1784 'g_', 'cg_', 'yg_', etc.. 

1785 """ 

1786 return TextObject( 

1787 event.current_buffer.document.last_non_blank_of_current_line_position(), 

1788 type=TextObjectType.INCLUSIVE, 

1789 ) 

1790 

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

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

1793 """ 

1794 Go to last character of previous word. 

1795 'ge', 'cge', 'yge', etc.. 

1796 """ 

1797 prev_end = event.current_buffer.document.find_previous_word_ending( 

1798 count=event.arg 

1799 ) 

1800 return TextObject( 

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

1802 ) 

1803 

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

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

1806 """ 

1807 Go to last character of previous WORD. 

1808 'gE', 'cgE', 'ygE', etc.. 

1809 """ 

1810 prev_end = event.current_buffer.document.find_previous_word_ending( 

1811 count=event.arg, WORD=True 

1812 ) 

1813 return TextObject( 

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

1815 ) 

1816 

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

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

1819 """ 

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

1821 """ 

1822 w = event.app.layout.current_window 

1823 buff = event.current_buffer 

1824 

1825 if w and w.render_info: 

1826 width = w.render_info.window_width 

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

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

1829 

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

1831 return TextObject(0) 

1832 

1833 @text_object("G") 

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

1835 """ 

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

1837 """ 

1838 buf = event.current_buffer 

1839 return TextObject( 

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

1841 - buf.cursor_position, 

1842 type=TextObjectType.LINEWISE, 

1843 ) 

1844 

1845 # 

1846 # *** Other *** 

1847 # 

1848 

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

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

1851 """ 

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

1853 example, 15G) 

1854 """ 

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

1856 

1857 for n in "123456789": 

1858 

1859 @handle( 

1860 n, 

1861 filter=vi_navigation_mode 

1862 | vi_selection_mode 

1863 | vi_waiting_for_text_object_mode, 

1864 ) 

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

1866 """ 

1867 Always handle numerics in navigation mode as arg. 

1868 """ 

1869 event.append_to_arg_count(event.data) 

1870 

1871 @handle( 

1872 "0", 

1873 filter=( 

1874 vi_navigation_mode | vi_selection_mode | vi_waiting_for_text_object_mode 

1875 ) 

1876 & has_arg, 

1877 ) 

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

1879 """ 

1880 Zero when an argument was already give. 

1881 """ 

1882 event.append_to_arg_count(event.data) 

1883 

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

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

1886 """ 

1887 Insert data at cursor position. 

1888 """ 

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

1890 

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

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

1893 """ 

1894 Replace single character at cursor position. 

1895 """ 

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

1897 event.current_buffer.cursor_position -= 1 

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

1899 

1900 @handle( 

1901 Keys.Any, 

1902 filter=vi_insert_multiple_mode, 

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

1904 ) 

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

1906 """ 

1907 Insert data at multiple cursor positions at once. 

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

1909 """ 

1910 buff = event.current_buffer 

1911 original_text = buff.text 

1912 

1913 # Construct new text. 

1914 text = [] 

1915 p = 0 

1916 

1917 for p2 in buff.multiple_cursor_positions: 

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

1919 text.append(event.data) 

1920 p = p2 

1921 

1922 text.append(original_text[p:]) 

1923 

1924 # Shift all cursor positions. 

1925 new_cursor_positions = [ 

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

1927 ] 

1928 

1929 # Set result. 

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

1931 buff.multiple_cursor_positions = new_cursor_positions 

1932 buff.cursor_position += 1 

1933 

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

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

1936 """ 

1937 Backspace, using multiple cursors. 

1938 """ 

1939 buff = event.current_buffer 

1940 original_text = buff.text 

1941 

1942 # Construct new text. 

1943 deleted_something = False 

1944 text = [] 

1945 p = 0 

1946 

1947 for p2 in buff.multiple_cursor_positions: 

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

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

1950 deleted_something = True 

1951 else: 

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

1953 p = p2 

1954 

1955 text.append(original_text[p:]) 

1956 

1957 if deleted_something: 

1958 # Shift all cursor positions. 

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

1960 new_cursor_positions = list(accumulate(lengths)) 

1961 

1962 # Set result. 

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

1964 buff.multiple_cursor_positions = new_cursor_positions 

1965 buff.cursor_position -= 1 

1966 else: 

1967 event.app.output.bell() 

1968 

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

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

1971 """ 

1972 Delete, using multiple cursors. 

1973 """ 

1974 buff = event.current_buffer 

1975 original_text = buff.text 

1976 

1977 # Construct new text. 

1978 deleted_something = False 

1979 text = [] 

1980 new_cursor_positions = [] 

1981 p = 0 

1982 

1983 for p2 in buff.multiple_cursor_positions: 

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

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

1986 # Don't delete across lines. 

1987 p = p2 

1988 else: 

1989 p = p2 + 1 

1990 deleted_something = True 

1991 

1992 text.append(original_text[p:]) 

1993 

1994 if deleted_something: 

1995 # Shift all cursor positions. 

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

1997 new_cursor_positions = list(accumulate(lengths)) 

1998 

1999 # Set result. 

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

2001 buff.multiple_cursor_positions = new_cursor_positions 

2002 else: 

2003 event.app.output.bell() 

2004 

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

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

2007 """ 

2008 Move all cursors to the left. 

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

2010 """ 

2011 buff = event.current_buffer 

2012 new_positions = [] 

2013 

2014 for p in buff.multiple_cursor_positions: 

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

2016 p -= 1 

2017 new_positions.append(p) 

2018 

2019 buff.multiple_cursor_positions = new_positions 

2020 

2021 if buff.document.cursor_position_col > 0: 

2022 buff.cursor_position -= 1 

2023 

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

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

2026 """ 

2027 Move all cursors to the right. 

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 row, column = buff.document.translate_index_to_position(p) 

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

2036 p += 1 

2037 new_positions.append(p) 

2038 

2039 buff.multiple_cursor_positions = new_positions 

2040 

2041 if not buff.document.is_cursor_at_the_end_of_line: 

2042 buff.cursor_position += 1 

2043 

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

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

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

2047 """ 

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

2049 """ 

2050 

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

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

2053 """ 

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

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

2056 """ 

2057 event.current_buffer.start_history_lines_completion() 

2058 

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

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

2061 """ 

2062 Complete file names. 

2063 """ 

2064 # TODO 

2065 pass 

2066 

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

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

2069 """ 

2070 Go into digraph mode. 

2071 """ 

2072 event.app.vi_state.waiting_for_digraph = True 

2073 

2074 @Condition 

2075 def digraph_symbol_1_given() -> bool: 

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

2077 

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

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

2080 """ 

2081 First digraph symbol. 

2082 """ 

2083 event.app.vi_state.digraph_symbol1 = event.data 

2084 

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

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

2087 """ 

2088 Insert digraph. 

2089 """ 

2090 try: 

2091 # Lookup. 

2092 code: tuple[str, str] = ( 

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

2094 event.data, 

2095 ) 

2096 if code not in DIGRAPHS: 

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

2098 symbol = DIGRAPHS[code] 

2099 except KeyError: 

2100 # Unknown digraph. 

2101 event.app.output.bell() 

2102 else: 

2103 # Insert digraph. 

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

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

2106 event.app.vi_state.waiting_for_digraph = False 

2107 finally: 

2108 event.app.vi_state.waiting_for_digraph = False 

2109 event.app.vi_state.digraph_symbol1 = None 

2110 

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

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

2113 """ 

2114 Go into normal mode for one single action. 

2115 """ 

2116 event.app.vi_state.temporary_navigation_mode = True 

2117 

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

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

2120 """ 

2121 Start recording macro. 

2122 """ 

2123 c = event.key_sequence[1].data 

2124 if c in vi_register_names: 

2125 vi_state = event.app.vi_state 

2126 

2127 vi_state.recording_register = c 

2128 vi_state.current_recording = "" 

2129 

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

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

2132 """ 

2133 Stop recording macro. 

2134 """ 

2135 vi_state = event.app.vi_state 

2136 

2137 # Store and stop recording. 

2138 if vi_state.recording_register: 

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

2140 vi_state.current_recording 

2141 ) 

2142 vi_state.recording_register = None 

2143 vi_state.current_recording = "" 

2144 

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

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

2147 """ 

2148 Execute macro. 

2149 

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

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

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

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

2154 `record_in_macro=True`. 

2155 """ 

2156 # Retrieve macro. 

2157 c = event.key_sequence[1].data 

2158 try: 

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

2160 except KeyError: 

2161 return 

2162 

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

2164 # Use vt100 parser for this. 

2165 keys: list[KeyPress] = [] 

2166 

2167 parser = Vt100Parser(keys.append) 

2168 parser.feed(macro.text) 

2169 parser.flush() 

2170 

2171 # Now feed keys back to the input processor. 

2172 for _ in range(event.arg): 

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

2174 

2175 return ConditionalKeyBindings(key_bindings, vi_mode) 

2176 

2177 

2178def load_vi_search_bindings() -> KeyBindingsBase: 

2179 key_bindings = KeyBindings() 

2180 handle = key_bindings.add 

2181 from . import search 

2182 

2183 @Condition 

2184 def search_buffer_is_empty() -> bool: 

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

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

2187 

2188 # Vi-style forward search. 

2189 handle( 

2190 "/", 

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

2192 )(search.start_forward_incremental_search) 

2193 handle( 

2194 "?", 

2195 filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, 

2196 )(search.start_forward_incremental_search) 

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

2198 

2199 # Vi-style backward search. 

2200 handle( 

2201 "?", 

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

2203 )(search.start_reverse_incremental_search) 

2204 handle( 

2205 "/", 

2206 filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, 

2207 )(search.start_reverse_incremental_search) 

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

2209 

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

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

2212 

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

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

2215 

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

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

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

2219 

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

2221 # `abort_search` would be a meaningful alternative. 

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

2223 

2224 return ConditionalKeyBindings(key_bindings, vi_mode)