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

954 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:07 +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, List, Optional, Tuple, TypeVar, Union 

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 def _indent(event: E) -> None: 

966 """ 

967 Indent lines. 

968 """ 

969 buffer = event.current_buffer 

970 current_row = buffer.document.cursor_position_row 

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

972 

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

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

975 """ 

976 Unindent lines. 

977 """ 

978 current_row = event.current_buffer.document.cursor_position_row 

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

980 

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

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

983 """ 

984 Open line above and enter insertion mode 

985 """ 

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

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

988 

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

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

991 """ 

992 Open line below and enter insertion mode 

993 """ 

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

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

996 

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

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

999 """ 

1000 Reverse case of current character and move cursor forward. 

1001 """ 

1002 buffer = event.current_buffer 

1003 c = buffer.document.current_char 

1004 

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

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

1007 

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

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

1010 """ 

1011 Lowercase current line. 

1012 """ 

1013 buff = event.current_buffer 

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

1015 

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

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

1018 """ 

1019 Uppercase current line. 

1020 """ 

1021 buff = event.current_buffer 

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

1023 

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

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

1026 """ 

1027 Swap case of the current line. 

1028 """ 

1029 buff = event.current_buffer 

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

1031 

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

1033 def _prev_occurence(event: E) -> None: 

1034 """ 

1035 Go to previous occurrence of this word. 

1036 """ 

1037 b = event.current_buffer 

1038 search_state = event.app.current_search_state 

1039 

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

1041 search_state.direction = SearchDirection.BACKWARD 

1042 

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

1044 

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

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

1047 """ 

1048 Go to next occurrence of this word. 

1049 """ 

1050 b = event.current_buffer 

1051 search_state = event.app.current_search_state 

1052 

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

1054 search_state.direction = SearchDirection.FORWARD 

1055 

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

1057 

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

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

1060 # TODO: go to begin of sentence. 

1061 # XXX: should become text_object. 

1062 pass 

1063 

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

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

1066 # TODO: go to end of sentence. 

1067 # XXX: should become text_object. 

1068 pass 

1069 

1070 operator = create_operator_decorator(key_bindings) 

1071 text_object = create_text_object_decorator(key_bindings) 

1072 

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

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

1075 """ 

1076 Unknown key binding while waiting for a text object. 

1077 """ 

1078 event.app.output.bell() 

1079 

1080 # 

1081 # *** Operators *** 

1082 # 

1083 

1084 def create_delete_and_change_operators( 

1085 delete_only: bool, with_register: bool = False 

1086 ) -> None: 

1087 """ 

1088 Delete and change operators. 

1089 

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

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

1092 """ 

1093 handler_keys: Iterable[str] 

1094 if with_register: 

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

1096 else: 

1097 handler_keys = "cd"[delete_only] 

1098 

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

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

1101 clipboard_data = None 

1102 buff = event.current_buffer 

1103 

1104 if text_object: 

1105 new_document, clipboard_data = text_object.cut(buff) 

1106 buff.document = new_document 

1107 

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

1109 if clipboard_data and clipboard_data.text: 

1110 if with_register: 

1111 reg_name = event.key_sequence[1].data 

1112 if reg_name in vi_register_names: 

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

1114 else: 

1115 event.app.clipboard.set_data(clipboard_data) 

1116 

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

1118 if not delete_only: 

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

1120 

1121 create_delete_and_change_operators(False, False) 

1122 create_delete_and_change_operators(False, True) 

1123 create_delete_and_change_operators(True, False) 

1124 create_delete_and_change_operators(True, True) 

1125 

1126 def create_transform_handler( 

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

1128 ) -> None: 

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

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

1131 """ 

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

1133 """ 

1134 buff = event.current_buffer 

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

1136 

1137 if start < end: 

1138 # Transform. 

1139 buff.transform_region( 

1140 buff.cursor_position + start, 

1141 buff.cursor_position + end, 

1142 transform_func, 

1143 ) 

1144 

1145 # Move cursor 

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

1147 

1148 for k, f, func in vi_transform_functions: 

1149 create_transform_handler(f, func, *k) 

1150 

1151 @operator("y") 

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

1153 """ 

1154 Yank operator. (Copy text.) 

1155 """ 

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

1157 if clipboard_data.text: 

1158 event.app.clipboard.set_data(clipboard_data) 

1159 

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

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

1162 """ 

1163 Yank selection to named register. 

1164 """ 

1165 c = event.key_sequence[1].data 

1166 if c in vi_register_names: 

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

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

1169 

1170 @operator(">") 

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

1172 """ 

1173 Indent. 

1174 """ 

1175 buff = event.current_buffer 

1176 from_, to = text_object.get_line_numbers(buff) 

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

1178 

1179 @operator("<") 

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

1181 """ 

1182 Unindent. 

1183 """ 

1184 buff = event.current_buffer 

1185 from_, to = text_object.get_line_numbers(buff) 

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

1187 

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

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

1190 """ 

1191 Reshape text. 

1192 """ 

1193 buff = event.current_buffer 

1194 from_, to = text_object.get_line_numbers(buff) 

1195 reshape_text(buff, from_, to) 

1196 

1197 # 

1198 # *** Text objects *** 

1199 # 

1200 

1201 @text_object("b") 

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

1203 """ 

1204 Move one word or token left. 

1205 """ 

1206 return TextObject( 

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

1208 or 0 

1209 ) 

1210 

1211 @text_object("B") 

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

1213 """ 

1214 Move one non-blank word left 

1215 """ 

1216 return TextObject( 

1217 event.current_buffer.document.find_start_of_previous_word( 

1218 count=event.arg, WORD=True 

1219 ) 

1220 or 0 

1221 ) 

1222 

1223 @text_object("$") 

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

1225 """ 

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

1227 """ 

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

1229 

1230 @text_object("w") 

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

1232 """ 

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

1234 """ 

1235 return TextObject( 

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

1237 or event.current_buffer.document.get_end_of_document_position() 

1238 ) 

1239 

1240 @text_object("W") 

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

1242 """ 

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

1244 """ 

1245 return TextObject( 

1246 event.current_buffer.document.find_next_word_beginning( 

1247 count=event.arg, WORD=True 

1248 ) 

1249 or event.current_buffer.document.get_end_of_document_position() 

1250 ) 

1251 

1252 @text_object("e") 

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

1254 """ 

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

1256 """ 

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

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

1259 

1260 @text_object("E") 

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

1262 """ 

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

1264 """ 

1265 end = event.current_buffer.document.find_next_word_ending( 

1266 count=event.arg, WORD=True 

1267 ) 

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

1269 

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

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

1272 """ 

1273 Inner 'word': ciw and diw 

1274 """ 

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

1276 return TextObject(start, end) 

1277 

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

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

1280 """ 

1281 A 'word': caw and daw 

1282 """ 

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

1284 include_trailing_whitespace=True 

1285 ) 

1286 return TextObject(start, end) 

1287 

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

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

1290 """ 

1291 Inner 'WORD': ciW and diW 

1292 """ 

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

1294 WORD=True 

1295 ) 

1296 return TextObject(start, end) 

1297 

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

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

1300 """ 

1301 A 'WORD': caw and daw 

1302 """ 

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

1304 WORD=True, include_trailing_whitespace=True 

1305 ) 

1306 return TextObject(start, end) 

1307 

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

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

1310 """ 

1311 Auto paragraph. 

1312 """ 

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

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

1315 return TextObject(start, end) 

1316 

1317 @text_object("^") 

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

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

1320 return TextObject( 

1321 event.current_buffer.document.get_start_of_line_position( 

1322 after_whitespace=True 

1323 ) 

1324 ) 

1325 

1326 @text_object("0") 

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

1328 """ 

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

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

1331 """ 

1332 return TextObject( 

1333 event.current_buffer.document.get_start_of_line_position( 

1334 after_whitespace=False 

1335 ) 

1336 ) 

1337 

1338 def create_ci_ca_handles( 

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

1340 ) -> None: 

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

1342 """ 

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

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

1345 """ 

1346 

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

1348 if ci_start == ci_end: 

1349 # Quotes 

1350 start = event.current_buffer.document.find_backwards( 

1351 ci_start, in_current_line=False 

1352 ) 

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

1354 else: 

1355 # Brackets 

1356 start = event.current_buffer.document.find_enclosing_bracket_left( 

1357 ci_start, ci_end 

1358 ) 

1359 end = event.current_buffer.document.find_enclosing_bracket_right( 

1360 ci_start, ci_end 

1361 ) 

1362 

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

1364 offset = 0 if inner else 1 

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

1366 else: 

1367 # Nothing found. 

1368 return TextObject(0) 

1369 

1370 if key is None: 

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

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

1373 else: 

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

1375 

1376 for inner in (False, True): 

1377 for ci_start, ci_end in [ 

1378 ('"', '"'), 

1379 ("'", "'"), 

1380 ("`", "`"), 

1381 ("[", "]"), 

1382 ("<", ">"), 

1383 ("{", "}"), 

1384 ("(", ")"), 

1385 ]: 

1386 create_ci_ca_handles(ci_start, ci_end, inner) 

1387 

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

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

1390 

1391 @text_object("{") 

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

1393 """ 

1394 Move to previous blank-line separated section. 

1395 Implements '{', 'c{', 'd{', 'y{' 

1396 """ 

1397 index = event.current_buffer.document.start_of_paragraph( 

1398 count=event.arg, before=True 

1399 ) 

1400 return TextObject(index) 

1401 

1402 @text_object("}") 

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

1404 """ 

1405 Move to next blank-line separated section. 

1406 Implements '}', 'c}', 'd}', 'y}' 

1407 """ 

1408 index = event.current_buffer.document.end_of_paragraph( 

1409 count=event.arg, after=True 

1410 ) 

1411 return TextObject(index) 

1412 

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

1414 def _next_occurence(event: E) -> TextObject: 

1415 """ 

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

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

1418 """ 

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

1420 match = event.current_buffer.document.find( 

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

1422 ) 

1423 if match: 

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

1425 else: 

1426 return TextObject(0) 

1427 

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

1429 def _previous_occurance(event: E) -> TextObject: 

1430 """ 

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

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

1433 """ 

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

1435 return TextObject( 

1436 event.current_buffer.document.find_backwards( 

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

1438 ) 

1439 or 0 

1440 ) 

1441 

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

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

1444 """ 

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

1446 """ 

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

1448 match = event.current_buffer.document.find( 

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

1450 ) 

1451 if match: 

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

1453 else: 

1454 return TextObject(0) 

1455 

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

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

1458 """ 

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

1460 """ 

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

1462 match = event.current_buffer.document.find_backwards( 

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

1464 ) 

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

1466 

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

1468 """ 

1469 Create ',' and ';' commands. 

1470 """ 

1471 

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

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

1474 """ 

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

1476 """ 

1477 pos: int | None = 0 

1478 vi_state = event.app.vi_state 

1479 

1480 type = TextObjectType.EXCLUSIVE 

1481 

1482 if vi_state.last_character_find: 

1483 char = vi_state.last_character_find.character 

1484 backwards = vi_state.last_character_find.backwards 

1485 

1486 if reverse: 

1487 backwards = not backwards 

1488 

1489 if backwards: 

1490 pos = event.current_buffer.document.find_backwards( 

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

1492 ) 

1493 else: 

1494 pos = event.current_buffer.document.find( 

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

1496 ) 

1497 type = TextObjectType.INCLUSIVE 

1498 if pos: 

1499 return TextObject(pos, type=type) 

1500 else: 

1501 return TextObject(0) 

1502 

1503 repeat(True) 

1504 repeat(False) 

1505 

1506 @text_object("h") 

1507 @text_object("left") 

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

1509 """ 

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

1511 """ 

1512 return TextObject( 

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

1514 ) 

1515 

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

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

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

1519 # `buffer.preferred_column`. 

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

1521 """ 

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

1523 """ 

1524 return TextObject( 

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

1526 type=TextObjectType.LINEWISE, 

1527 ) 

1528 

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

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

1531 """ 

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

1533 """ 

1534 return TextObject( 

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

1536 type=TextObjectType.LINEWISE, 

1537 ) 

1538 

1539 @text_object("l") 

1540 @text_object(" ") 

1541 @text_object("right") 

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

1543 """ 

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

1545 """ 

1546 return TextObject( 

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

1548 ) 

1549 

1550 @text_object("H") 

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

1552 """ 

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

1554 Implements 'cH', 'dH', 'H'. 

1555 """ 

1556 w = event.app.layout.current_window 

1557 b = event.current_buffer 

1558 

1559 if w and w.render_info: 

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

1561 # move to the start of the visible area. 

1562 pos = ( 

1563 b.document.translate_row_col_to_index( 

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

1565 ) 

1566 - b.cursor_position 

1567 ) 

1568 

1569 else: 

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

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

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

1573 

1574 @text_object("M") 

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

1576 """ 

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

1578 Implements 'cM', 'dM', 'M'. 

1579 """ 

1580 w = event.app.layout.current_window 

1581 b = event.current_buffer 

1582 

1583 if w and w.render_info: 

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

1585 # move to the center of the visible area. 

1586 pos = ( 

1587 b.document.translate_row_col_to_index( 

1588 w.render_info.center_visible_line(), 0 

1589 ) 

1590 - b.cursor_position 

1591 ) 

1592 

1593 else: 

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

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

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

1597 

1598 @text_object("L") 

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

1600 """ 

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

1602 """ 

1603 w = event.app.layout.current_window 

1604 b = event.current_buffer 

1605 

1606 if w and w.render_info: 

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

1608 # move to the end of the visible area. 

1609 pos = ( 

1610 b.document.translate_row_col_to_index( 

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

1612 ) 

1613 - b.cursor_position 

1614 ) 

1615 

1616 else: 

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

1618 pos = len(b.document.text_after_cursor) 

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

1620 

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

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

1623 """ 

1624 Search next. 

1625 """ 

1626 buff = event.current_buffer 

1627 search_state = event.app.current_search_state 

1628 

1629 cursor_position = buff.get_search_position( 

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

1631 ) 

1632 return TextObject(cursor_position - buff.cursor_position) 

1633 

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

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

1636 """ 

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

1638 """ 

1639 search_state = event.app.current_search_state 

1640 

1641 event.current_buffer.apply_search( 

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

1643 ) 

1644 

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

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

1647 """ 

1648 Search previous. 

1649 """ 

1650 buff = event.current_buffer 

1651 search_state = event.app.current_search_state 

1652 

1653 cursor_position = buff.get_search_position( 

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

1655 ) 

1656 return TextObject(cursor_position - buff.cursor_position) 

1657 

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

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

1660 """ 

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

1662 """ 

1663 search_state = event.app.current_search_state 

1664 

1665 event.current_buffer.apply_search( 

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

1667 ) 

1668 

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

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

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

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

1673 """ 

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

1675 """ 

1676 b = event.current_buffer 

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

1678 

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

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

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

1682 """ 

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

1684 """ 

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

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

1687 # again. 

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

1689 

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

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

1692 """ 

1693 Center Window vertically around cursor. 

1694 """ 

1695 w = event.app.layout.current_window 

1696 b = event.current_buffer 

1697 

1698 if w and w.render_info: 

1699 info = w.render_info 

1700 

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

1702 # containing the cursor in the center. 

1703 scroll_height = info.window_height // 2 

1704 

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

1706 height = 0 

1707 while y > 0: 

1708 line_height = info.get_height_for_line(y) 

1709 

1710 if height + line_height < scroll_height: 

1711 height += line_height 

1712 y -= 1 

1713 else: 

1714 break 

1715 

1716 w.vertical_scroll = y 

1717 

1718 @text_object("%") 

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

1720 """ 

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

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

1723 """ 

1724 buffer = event.current_buffer 

1725 

1726 if event._arg: 

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

1728 # row in the file. 

1729 if 0 < event.arg <= 100: 

1730 absolute_index = buffer.document.translate_row_col_to_index( 

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

1732 ) 

1733 return TextObject( 

1734 absolute_index - buffer.document.cursor_position, 

1735 type=TextObjectType.LINEWISE, 

1736 ) 

1737 else: 

1738 return TextObject(0) # Do nothing. 

1739 

1740 else: 

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

1742 match = buffer.document.find_matching_bracket_position() 

1743 if match: 

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

1745 else: 

1746 return TextObject(0) 

1747 

1748 @text_object("|") 

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

1750 """ 

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

1752 number keys, for example, 20|). 

1753 """ 

1754 return TextObject( 

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

1756 ) 

1757 

1758 @text_object("g", "g") 

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

1760 """ 

1761 Go to the start of the very first line. 

1762 Implements 'gg', 'cgg', 'ygg' 

1763 """ 

1764 d = event.current_buffer.document 

1765 

1766 if event._arg: 

1767 # Move to the given line. 

1768 return TextObject( 

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

1770 type=TextObjectType.LINEWISE, 

1771 ) 

1772 else: 

1773 # Move to the top of the input. 

1774 return TextObject( 

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

1776 ) 

1777 

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

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

1780 """ 

1781 Go to last non-blank of line. 

1782 'g_', 'cg_', 'yg_', etc.. 

1783 """ 

1784 return TextObject( 

1785 event.current_buffer.document.last_non_blank_of_current_line_position(), 

1786 type=TextObjectType.INCLUSIVE, 

1787 ) 

1788 

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

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

1791 """ 

1792 Go to last character of previous word. 

1793 'ge', 'cge', 'yge', etc.. 

1794 """ 

1795 prev_end = event.current_buffer.document.find_previous_word_ending( 

1796 count=event.arg 

1797 ) 

1798 return TextObject( 

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

1800 ) 

1801 

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

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

1804 """ 

1805 Go to last character of previous WORD. 

1806 'gE', 'cgE', 'ygE', etc.. 

1807 """ 

1808 prev_end = event.current_buffer.document.find_previous_word_ending( 

1809 count=event.arg, WORD=True 

1810 ) 

1811 return TextObject( 

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

1813 ) 

1814 

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

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

1817 """ 

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

1819 """ 

1820 w = event.app.layout.current_window 

1821 buff = event.current_buffer 

1822 

1823 if w and w.render_info: 

1824 width = w.render_info.window_width 

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

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

1827 

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

1829 return TextObject(0) 

1830 

1831 @text_object("G") 

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

1833 """ 

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

1835 """ 

1836 buf = event.current_buffer 

1837 return TextObject( 

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

1839 - buf.cursor_position, 

1840 type=TextObjectType.LINEWISE, 

1841 ) 

1842 

1843 # 

1844 # *** Other *** 

1845 # 

1846 

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

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

1849 """ 

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

1851 example, 15G) 

1852 """ 

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

1854 

1855 for n in "123456789": 

1856 

1857 @handle( 

1858 n, 

1859 filter=vi_navigation_mode 

1860 | vi_selection_mode 

1861 | vi_waiting_for_text_object_mode, 

1862 ) 

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

1864 """ 

1865 Always handle numerics in navigation mode as arg. 

1866 """ 

1867 event.append_to_arg_count(event.data) 

1868 

1869 @handle( 

1870 "0", 

1871 filter=( 

1872 vi_navigation_mode | vi_selection_mode | vi_waiting_for_text_object_mode 

1873 ) 

1874 & has_arg, 

1875 ) 

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

1877 """ 

1878 Zero when an argument was already give. 

1879 """ 

1880 event.append_to_arg_count(event.data) 

1881 

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

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

1884 """ 

1885 Insert data at cursor position. 

1886 """ 

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

1888 

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

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

1891 """ 

1892 Replace single character at cursor position. 

1893 """ 

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

1895 event.current_buffer.cursor_position -= 1 

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

1897 

1898 @handle( 

1899 Keys.Any, 

1900 filter=vi_insert_multiple_mode, 

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

1902 ) 

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

1904 """ 

1905 Insert data at multiple cursor positions at once. 

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

1907 """ 

1908 buff = event.current_buffer 

1909 original_text = buff.text 

1910 

1911 # Construct new text. 

1912 text = [] 

1913 p = 0 

1914 

1915 for p2 in buff.multiple_cursor_positions: 

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

1917 text.append(event.data) 

1918 p = p2 

1919 

1920 text.append(original_text[p:]) 

1921 

1922 # Shift all cursor positions. 

1923 new_cursor_positions = [ 

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

1925 ] 

1926 

1927 # Set result. 

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

1929 buff.multiple_cursor_positions = new_cursor_positions 

1930 buff.cursor_position += 1 

1931 

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

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

1934 """ 

1935 Backspace, using multiple cursors. 

1936 """ 

1937 buff = event.current_buffer 

1938 original_text = buff.text 

1939 

1940 # Construct new text. 

1941 deleted_something = False 

1942 text = [] 

1943 p = 0 

1944 

1945 for p2 in buff.multiple_cursor_positions: 

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

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

1948 deleted_something = True 

1949 else: 

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

1951 p = p2 

1952 

1953 text.append(original_text[p:]) 

1954 

1955 if deleted_something: 

1956 # Shift all cursor positions. 

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

1958 new_cursor_positions = list(accumulate(lengths)) 

1959 

1960 # Set result. 

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

1962 buff.multiple_cursor_positions = new_cursor_positions 

1963 buff.cursor_position -= 1 

1964 else: 

1965 event.app.output.bell() 

1966 

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

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

1969 """ 

1970 Delete, using multiple cursors. 

1971 """ 

1972 buff = event.current_buffer 

1973 original_text = buff.text 

1974 

1975 # Construct new text. 

1976 deleted_something = False 

1977 text = [] 

1978 new_cursor_positions = [] 

1979 p = 0 

1980 

1981 for p2 in buff.multiple_cursor_positions: 

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

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

1984 # Don't delete across lines. 

1985 p = p2 

1986 else: 

1987 p = p2 + 1 

1988 deleted_something = True 

1989 

1990 text.append(original_text[p:]) 

1991 

1992 if deleted_something: 

1993 # Shift all cursor positions. 

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

1995 new_cursor_positions = list(accumulate(lengths)) 

1996 

1997 # Set result. 

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

1999 buff.multiple_cursor_positions = new_cursor_positions 

2000 else: 

2001 event.app.output.bell() 

2002 

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

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

2005 """ 

2006 Move all cursors to the left. 

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

2008 """ 

2009 buff = event.current_buffer 

2010 new_positions = [] 

2011 

2012 for p in buff.multiple_cursor_positions: 

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

2014 p -= 1 

2015 new_positions.append(p) 

2016 

2017 buff.multiple_cursor_positions = new_positions 

2018 

2019 if buff.document.cursor_position_col > 0: 

2020 buff.cursor_position -= 1 

2021 

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

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

2024 """ 

2025 Move all cursors to the right. 

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

2027 """ 

2028 buff = event.current_buffer 

2029 new_positions = [] 

2030 

2031 for p in buff.multiple_cursor_positions: 

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

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

2034 p += 1 

2035 new_positions.append(p) 

2036 

2037 buff.multiple_cursor_positions = new_positions 

2038 

2039 if not buff.document.is_cursor_at_the_end_of_line: 

2040 buff.cursor_position += 1 

2041 

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

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

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

2045 """ 

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

2047 """ 

2048 

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

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

2051 """ 

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

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

2054 """ 

2055 event.current_buffer.start_history_lines_completion() 

2056 

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

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

2059 """ 

2060 Complete file names. 

2061 """ 

2062 # TODO 

2063 pass 

2064 

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

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

2067 """ 

2068 Go into digraph mode. 

2069 """ 

2070 event.app.vi_state.waiting_for_digraph = True 

2071 

2072 @Condition 

2073 def digraph_symbol_1_given() -> bool: 

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

2075 

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

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

2078 """ 

2079 First digraph symbol. 

2080 """ 

2081 event.app.vi_state.digraph_symbol1 = event.data 

2082 

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

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

2085 """ 

2086 Insert digraph. 

2087 """ 

2088 try: 

2089 # Lookup. 

2090 code: tuple[str, str] = ( 

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

2092 event.data, 

2093 ) 

2094 if code not in DIGRAPHS: 

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

2096 symbol = DIGRAPHS[code] 

2097 except KeyError: 

2098 # Unknown digraph. 

2099 event.app.output.bell() 

2100 else: 

2101 # Insert digraph. 

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

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

2104 event.app.vi_state.waiting_for_digraph = False 

2105 finally: 

2106 event.app.vi_state.waiting_for_digraph = False 

2107 event.app.vi_state.digraph_symbol1 = None 

2108 

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

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

2111 """ 

2112 Go into normal mode for one single action. 

2113 """ 

2114 event.app.vi_state.temporary_navigation_mode = True 

2115 

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

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

2118 """ 

2119 Start recording macro. 

2120 """ 

2121 c = event.key_sequence[1].data 

2122 if c in vi_register_names: 

2123 vi_state = event.app.vi_state 

2124 

2125 vi_state.recording_register = c 

2126 vi_state.current_recording = "" 

2127 

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

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

2130 """ 

2131 Stop recording macro. 

2132 """ 

2133 vi_state = event.app.vi_state 

2134 

2135 # Store and stop recording. 

2136 if vi_state.recording_register: 

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

2138 vi_state.current_recording 

2139 ) 

2140 vi_state.recording_register = None 

2141 vi_state.current_recording = "" 

2142 

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

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

2145 """ 

2146 Execute macro. 

2147 

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

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

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

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

2152 `record_in_macro=True`. 

2153 """ 

2154 # Retrieve macro. 

2155 c = event.key_sequence[1].data 

2156 try: 

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

2158 except KeyError: 

2159 return 

2160 

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

2162 # Use vt100 parser for this. 

2163 keys: list[KeyPress] = [] 

2164 

2165 parser = Vt100Parser(keys.append) 

2166 parser.feed(macro.text) 

2167 parser.flush() 

2168 

2169 # Now feed keys back to the input processor. 

2170 for _ in range(event.arg): 

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

2172 

2173 return ConditionalKeyBindings(key_bindings, vi_mode) 

2174 

2175 

2176def load_vi_search_bindings() -> KeyBindingsBase: 

2177 key_bindings = KeyBindings() 

2178 handle = key_bindings.add 

2179 from . import search 

2180 

2181 @Condition 

2182 def search_buffer_is_empty() -> bool: 

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

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

2185 

2186 # Vi-style forward search. 

2187 handle( 

2188 "/", 

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

2190 )(search.start_forward_incremental_search) 

2191 handle( 

2192 "?", 

2193 filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, 

2194 )(search.start_forward_incremental_search) 

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

2196 

2197 # Vi-style backward search. 

2198 handle( 

2199 "?", 

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

2201 )(search.start_reverse_incremental_search) 

2202 handle( 

2203 "/", 

2204 filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, 

2205 )(search.start_reverse_incremental_search) 

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

2207 

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

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

2210 

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

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

2213 

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

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

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

2217 

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

2219 # `abort_search` would be a meaningful alternative. 

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

2221 

2222 return ConditionalKeyBindings(key_bindings, vi_mode)