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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

960 statements  

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 

374@Condition 

375def is_returnable() -> bool: 

376 return get_app().current_buffer.is_returnable 

377 

378 

379@Condition 

380def in_block_selection() -> bool: 

381 buff = get_app().current_buffer 

382 return bool( 

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

384 ) 

385 

386 

387@Condition 

388def digraph_symbol_1_given() -> bool: 

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

390 

391 

392@Condition 

393def search_buffer_is_empty() -> bool: 

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

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

396 

397 

398@Condition 

399def tilde_operator() -> bool: 

400 return get_app().vi_state.tilde_operator 

401 

402 

403def load_vi_bindings() -> KeyBindingsBase: 

404 """ 

405 Vi extensions. 

406 

407 # Overview of Readline Vi commands: 

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

409 """ 

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

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

412 # read-only buffer. 

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

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

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

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

417 # bindings that do text manipulation. 

418 

419 key_bindings = KeyBindings() 

420 handle = key_bindings.add 

421 

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

423 # ViState says different.) 

424 

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

426 

427 vi_transform_functions: list[TransformFunction] = [ 

428 # Rot 13 transformation 

429 ( 

430 ("g", "?"), 

431 Always(), 

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

433 ), 

434 # To lowercase 

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

436 # To uppercase. 

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

438 # Swap case. 

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

440 ( 

441 ("~",), 

442 tilde_operator, 

443 lambda string: string.swapcase(), 

444 ), 

445 ] 

446 

447 # Insert a character literally (quoted insert). 

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

449 

450 @handle("escape") 

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

452 """ 

453 Escape goes to vi navigation mode. 

454 """ 

455 buffer = event.current_buffer 

456 vi_state = event.app.vi_state 

457 

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

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

460 

461 vi_state.input_mode = InputMode.NAVIGATION 

462 

463 if bool(buffer.selection_state): 

464 buffer.exit_selection() 

465 

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

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

468 """ 

469 Arrow up in selection mode. 

470 """ 

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

472 

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

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

475 """ 

476 Arrow down in selection mode. 

477 """ 

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

479 

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

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

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

483 """ 

484 Arrow up and ControlP in navigation mode go up. 

485 """ 

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

487 

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

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

490 """ 

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

492 line. 

493 """ 

494 event.current_buffer.auto_up( 

495 count=event.arg, go_to_start_of_line_if_history_changes=True 

496 ) 

497 

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

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

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

501 """ 

502 Arrow down and Control-N in navigation mode. 

503 """ 

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

505 

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

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

508 """ 

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

510 """ 

511 event.current_buffer.auto_down( 

512 count=event.arg, go_to_start_of_line_if_history_changes=True 

513 ) 

514 

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

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

517 """ 

518 In navigation-mode, move cursor. 

519 """ 

520 event.current_buffer.cursor_position += ( 

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

522 ) 

523 

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

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

526 b = event.current_buffer 

527 

528 if b.complete_state: 

529 b.complete_next() 

530 else: 

531 b.start_completion(select_first=True) 

532 

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

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

535 """ 

536 Control-P: To previous completion. 

537 """ 

538 b = event.current_buffer 

539 

540 if b.complete_state: 

541 b.complete_previous() 

542 else: 

543 b.start_completion(select_last=True) 

544 

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

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

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

548 """ 

549 Accept current completion. 

550 """ 

551 event.current_buffer.complete_state = None 

552 

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

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

555 """ 

556 Cancel completion. Go back to originally typed text. 

557 """ 

558 event.current_buffer.cancel_completion() 

559 

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

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

562 get_by_name("accept-line") 

563 ) 

564 

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

566 # has been marked as single line. 

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

568 

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

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

571 """ 

572 Go to the beginning of next line. 

573 """ 

574 b = event.current_buffer 

575 b.cursor_down(count=event.arg) 

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

577 after_whitespace=True 

578 ) 

579 

580 # ** In navigation mode ** 

581 

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

583 

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

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

586 """ 

587 Pressing the Insert key. 

588 """ 

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

590 

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

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

593 """ 

594 Pressing the Insert key. 

595 """ 

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

597 

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

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

600 # read-only buffers. 

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

602 event.current_buffer.cursor_position += ( 

603 event.current_buffer.document.get_cursor_right_position() 

604 ) 

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

606 

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

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

609 event.current_buffer.cursor_position += ( 

610 event.current_buffer.document.get_end_of_line_position() 

611 ) 

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

613 

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

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

616 """ 

617 Change to end of line. 

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

619 """ 

620 buffer = event.current_buffer 

621 

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

623 event.app.clipboard.set_text(deleted) 

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

625 

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

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

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

629 """ 

630 Change current line 

631 """ 

632 buffer = event.current_buffer 

633 

634 # We copy the whole line. 

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

636 event.app.clipboard.set_data(data) 

637 

638 # But we delete after the whitespace 

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

640 after_whitespace=True 

641 ) 

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

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

644 

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

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

647 """ 

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

649 """ 

650 buffer = event.current_buffer 

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

652 event.app.clipboard.set_text(deleted) 

653 

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

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

656 """ 

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

658 """ 

659 buffer = event.current_buffer 

660 

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

662 lines = buffer.document.lines 

663 

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

665 deleted = "\n".join( 

666 lines[ 

667 buffer.document.cursor_position_row : buffer.document.cursor_position_row 

668 + event.arg 

669 ] 

670 ) 

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

672 

673 # Set new text. 

674 if before and after: 

675 before = before + "\n" 

676 

677 # Set text and cursor position. 

678 buffer.document = Document( 

679 text=before + after, 

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

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

682 ) 

683 

684 # Set clipboard data 

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

686 

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

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

689 """ 

690 Cut selection. 

691 ('x' is not an operator.) 

692 """ 

693 clipboard_data = event.current_buffer.cut_selection() 

694 event.app.clipboard.set_data(clipboard_data) 

695 

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

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

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

699 

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

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

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

703 event.current_buffer.cursor_position += ( 

704 event.current_buffer.document.get_start_of_line_position( 

705 after_whitespace=True 

706 ) 

707 ) 

708 

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

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

711 """ 

712 Insert in block selection mode. 

713 """ 

714 buff = event.current_buffer 

715 

716 # Store all cursor positions. 

717 positions = [] 

718 

719 if after: 

720 

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

722 return from_to[1] 

723 

724 else: 

725 

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

727 return from_to[0] 

728 

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

730 positions.append(get_pos(from_to)) 

731 if i == 0: 

732 buff.cursor_position = get_pos(from_to) 

733 

734 buff.multiple_cursor_positions = positions 

735 

736 # Go to 'INSERT_MULTIPLE' mode. 

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

738 buff.exit_selection() 

739 

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

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

742 insert_in_block_selection(event, after=True) 

743 

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

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

746 """ 

747 Join lines. 

748 """ 

749 for i in range(event.arg): 

750 event.current_buffer.join_next_line() 

751 

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

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

754 """ 

755 Join lines without space. 

756 """ 

757 for i in range(event.arg): 

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

759 

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

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

762 """ 

763 Join selected lines. 

764 """ 

765 event.current_buffer.join_selected_lines() 

766 

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

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

769 """ 

770 Join selected lines without space. 

771 """ 

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

773 

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

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

776 """ 

777 Paste after 

778 """ 

779 event.current_buffer.paste_clipboard_data( 

780 event.app.clipboard.get_data(), 

781 count=event.arg, 

782 paste_mode=PasteMode.VI_AFTER, 

783 ) 

784 

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

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

787 """ 

788 Paste before 

789 """ 

790 event.current_buffer.paste_clipboard_data( 

791 event.app.clipboard.get_data(), 

792 count=event.arg, 

793 paste_mode=PasteMode.VI_BEFORE, 

794 ) 

795 

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

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

798 """ 

799 Paste from named register. 

800 """ 

801 c = event.key_sequence[1].data 

802 if c in vi_register_names: 

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

804 if data: 

805 event.current_buffer.paste_clipboard_data( 

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

807 ) 

808 

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

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

811 """ 

812 Paste (before) from named register. 

813 """ 

814 c = event.key_sequence[1].data 

815 if c in vi_register_names: 

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

817 if data: 

818 event.current_buffer.paste_clipboard_data( 

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

820 ) 

821 

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

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

824 """ 

825 Go to 'replace-single'-mode. 

826 """ 

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

828 

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

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

831 """ 

832 Go to 'replace'-mode. 

833 """ 

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

835 

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

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

838 """ 

839 Substitute with new text 

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

841 """ 

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

843 event.app.clipboard.set_text(text) 

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

845 

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

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

848 for i in range(event.arg): 

849 event.current_buffer.undo() 

850 

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

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

853 """ 

854 Start lines selection. 

855 """ 

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

857 

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

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

860 """ 

861 Enter block selection mode. 

862 """ 

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

864 

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

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

867 """ 

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

869 selection mode. 

870 """ 

871 selection_state = event.current_buffer.selection_state 

872 

873 if selection_state is not None: 

874 if selection_state.type != SelectionType.LINES: 

875 selection_state.type = SelectionType.LINES 

876 else: 

877 event.current_buffer.exit_selection() 

878 

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

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

881 """ 

882 Enter character selection mode. 

883 """ 

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

885 

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

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

888 """ 

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

890 to character selection mode. 

891 """ 

892 selection_state = event.current_buffer.selection_state 

893 

894 if selection_state is not None: 

895 if selection_state.type != SelectionType.CHARACTERS: 

896 selection_state.type = SelectionType.CHARACTERS 

897 else: 

898 event.current_buffer.exit_selection() 

899 

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

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

902 """ 

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

904 selection mode. 

905 """ 

906 selection_state = event.current_buffer.selection_state 

907 

908 if selection_state is not None: 

909 if selection_state.type != SelectionType.BLOCK: 

910 selection_state.type = SelectionType.BLOCK 

911 else: 

912 event.current_buffer.exit_selection() 

913 

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

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

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

917 """ 

918 Switch from visual linewise mode to visual characterwise mode. 

919 """ 

920 buffer = event.current_buffer 

921 

922 if ( 

923 buffer.selection_state 

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

925 ): 

926 buffer.selection_state.type = SelectionType.CHARACTERS 

927 

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

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

930 """ 

931 Delete character. 

932 """ 

933 buff = event.current_buffer 

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

935 if count: 

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

937 event.app.clipboard.set_text(text) 

938 

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

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

941 buff = event.current_buffer 

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

943 if count: 

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

945 event.app.clipboard.set_text(text) 

946 

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

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

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

950 """ 

951 Yank the whole line. 

952 """ 

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

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

955 

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

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

958 """ 

959 Move to first non whitespace of next line 

960 """ 

961 buffer = event.current_buffer 

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

963 count=event.arg 

964 ) 

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

966 after_whitespace=True 

967 ) 

968 

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

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

971 """ 

972 Move to first non whitespace of previous line 

973 """ 

974 buffer = event.current_buffer 

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

976 count=event.arg 

977 ) 

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

979 after_whitespace=True 

980 ) 

981 

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

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

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

985 """ 

986 Indent lines. 

987 """ 

988 buffer = event.current_buffer 

989 current_row = buffer.document.cursor_position_row 

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

991 

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

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

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

995 """ 

996 Unindent lines. 

997 """ 

998 current_row = event.current_buffer.document.cursor_position_row 

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

1000 

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

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

1003 """ 

1004 Open line above and enter insertion mode 

1005 """ 

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

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

1008 

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

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

1011 """ 

1012 Open line below and enter insertion mode 

1013 """ 

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

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

1016 

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

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

1019 """ 

1020 Reverse case of current character and move cursor forward. 

1021 """ 

1022 buffer = event.current_buffer 

1023 c = buffer.document.current_char 

1024 

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

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

1027 

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

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

1030 """ 

1031 Lowercase current line. 

1032 """ 

1033 buff = event.current_buffer 

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

1035 

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

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

1038 """ 

1039 Uppercase current line. 

1040 """ 

1041 buff = event.current_buffer 

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

1043 

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

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

1046 """ 

1047 Swap case of the current line. 

1048 """ 

1049 buff = event.current_buffer 

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

1051 

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

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

1054 """ 

1055 Go to previous occurrence of this word. 

1056 """ 

1057 b = event.current_buffer 

1058 search_state = event.app.current_search_state 

1059 

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

1061 search_state.direction = SearchDirection.BACKWARD 

1062 

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

1064 

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

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

1067 """ 

1068 Go to next occurrence of this word. 

1069 """ 

1070 b = event.current_buffer 

1071 search_state = event.app.current_search_state 

1072 

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

1074 search_state.direction = SearchDirection.FORWARD 

1075 

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

1077 

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

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

1080 # TODO: go to begin of sentence. 

1081 # XXX: should become text_object. 

1082 pass 

1083 

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

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

1086 # TODO: go to end of sentence. 

1087 # XXX: should become text_object. 

1088 pass 

1089 

1090 operator = create_operator_decorator(key_bindings) 

1091 text_object = create_text_object_decorator(key_bindings) 

1092 

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

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

1095 """ 

1096 Unknown key binding while waiting for a text object. 

1097 """ 

1098 event.app.output.bell() 

1099 

1100 # 

1101 # *** Operators *** 

1102 # 

1103 

1104 def create_delete_and_change_operators( 

1105 delete_only: bool, with_register: bool = False 

1106 ) -> None: 

1107 """ 

1108 Delete and change operators. 

1109 

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

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

1112 """ 

1113 handler_keys: Iterable[str] 

1114 if with_register: 

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

1116 else: 

1117 handler_keys = "cd"[delete_only] 

1118 

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

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

1121 clipboard_data = None 

1122 buff = event.current_buffer 

1123 

1124 if text_object: 

1125 new_document, clipboard_data = text_object.cut(buff) 

1126 buff.document = new_document 

1127 

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

1129 if clipboard_data and clipboard_data.text: 

1130 if with_register: 

1131 reg_name = event.key_sequence[1].data 

1132 if reg_name in vi_register_names: 

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

1134 else: 

1135 event.app.clipboard.set_data(clipboard_data) 

1136 

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

1138 if not delete_only: 

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

1140 

1141 create_delete_and_change_operators(False, False) 

1142 create_delete_and_change_operators(False, True) 

1143 create_delete_and_change_operators(True, False) 

1144 create_delete_and_change_operators(True, True) 

1145 

1146 def create_transform_handler( 

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

1148 ) -> None: 

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

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

1151 """ 

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

1153 """ 

1154 buff = event.current_buffer 

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

1156 

1157 if start < end: 

1158 # Transform. 

1159 buff.transform_region( 

1160 buff.cursor_position + start, 

1161 buff.cursor_position + end, 

1162 transform_func, 

1163 ) 

1164 

1165 # Move cursor 

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

1167 

1168 for k, f, func in vi_transform_functions: 

1169 create_transform_handler(f, func, *k) 

1170 

1171 @operator("y") 

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

1173 """ 

1174 Yank operator. (Copy text.) 

1175 """ 

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

1177 if clipboard_data.text: 

1178 event.app.clipboard.set_data(clipboard_data) 

1179 

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

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

1182 """ 

1183 Yank selection to named register. 

1184 """ 

1185 c = event.key_sequence[1].data 

1186 if c in vi_register_names: 

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

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

1189 

1190 @operator(">") 

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

1192 """ 

1193 Indent. 

1194 """ 

1195 buff = event.current_buffer 

1196 from_, to = text_object.get_line_numbers(buff) 

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

1198 

1199 @operator("<") 

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

1201 """ 

1202 Unindent. 

1203 """ 

1204 buff = event.current_buffer 

1205 from_, to = text_object.get_line_numbers(buff) 

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

1207 

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

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

1210 """ 

1211 Reshape text. 

1212 """ 

1213 buff = event.current_buffer 

1214 from_, to = text_object.get_line_numbers(buff) 

1215 reshape_text(buff, from_, to) 

1216 

1217 # 

1218 # *** Text objects *** 

1219 # 

1220 

1221 @text_object("b") 

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

1223 """ 

1224 Move one word or token left. 

1225 """ 

1226 return TextObject( 

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

1228 or 0 

1229 ) 

1230 

1231 @text_object("B") 

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

1233 """ 

1234 Move one non-blank word left 

1235 """ 

1236 return TextObject( 

1237 event.current_buffer.document.find_start_of_previous_word( 

1238 count=event.arg, WORD=True 

1239 ) 

1240 or 0 

1241 ) 

1242 

1243 @text_object("$") 

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

1245 """ 

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

1247 """ 

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

1249 

1250 @text_object("w") 

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

1252 """ 

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

1254 """ 

1255 return TextObject( 

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

1257 or event.current_buffer.document.get_end_of_document_position() 

1258 ) 

1259 

1260 @text_object("W") 

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

1262 """ 

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

1264 """ 

1265 return TextObject( 

1266 event.current_buffer.document.find_next_word_beginning( 

1267 count=event.arg, WORD=True 

1268 ) 

1269 or event.current_buffer.document.get_end_of_document_position() 

1270 ) 

1271 

1272 @text_object("e") 

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

1274 """ 

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

1276 """ 

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

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

1279 

1280 @text_object("E") 

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

1282 """ 

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

1284 """ 

1285 end = event.current_buffer.document.find_next_word_ending( 

1286 count=event.arg, WORD=True 

1287 ) 

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

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 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 include_trailing_whitespace=True 

1305 ) 

1306 return TextObject(start, end) 

1307 

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

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

1310 """ 

1311 Inner 'WORD': ciW and diW 

1312 """ 

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

1314 WORD=True 

1315 ) 

1316 return TextObject(start, end) 

1317 

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

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

1320 """ 

1321 A 'WORD': caw and daw 

1322 """ 

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

1324 WORD=True, include_trailing_whitespace=True 

1325 ) 

1326 return TextObject(start, end) 

1327 

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

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

1330 """ 

1331 Auto paragraph. 

1332 """ 

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

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

1335 return TextObject(start, end) 

1336 

1337 @text_object("^") 

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

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

1340 return TextObject( 

1341 event.current_buffer.document.get_start_of_line_position( 

1342 after_whitespace=True 

1343 ) 

1344 ) 

1345 

1346 @text_object("0") 

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

1348 """ 

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

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

1351 """ 

1352 return TextObject( 

1353 event.current_buffer.document.get_start_of_line_position( 

1354 after_whitespace=False 

1355 ) 

1356 ) 

1357 

1358 def create_ci_ca_handles( 

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

1360 ) -> None: 

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

1362 """ 

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

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

1365 """ 

1366 

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

1368 if ci_start == ci_end: 

1369 # Quotes 

1370 start = event.current_buffer.document.find_backwards( 

1371 ci_start, in_current_line=False 

1372 ) 

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

1374 else: 

1375 # Brackets 

1376 start = event.current_buffer.document.find_enclosing_bracket_left( 

1377 ci_start, ci_end 

1378 ) 

1379 end = event.current_buffer.document.find_enclosing_bracket_right( 

1380 ci_start, ci_end 

1381 ) 

1382 

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

1384 offset = 0 if inner else 1 

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

1386 else: 

1387 # Nothing found. 

1388 return TextObject(0) 

1389 

1390 if key is None: 

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

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

1393 else: 

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

1395 

1396 for inner in (False, True): 

1397 for ci_start, ci_end in [ 

1398 ('"', '"'), 

1399 ("'", "'"), 

1400 ("`", "`"), 

1401 ("[", "]"), 

1402 ("<", ">"), 

1403 ("{", "}"), 

1404 ("(", ")"), 

1405 ]: 

1406 create_ci_ca_handles(ci_start, ci_end, inner) 

1407 

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

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

1410 

1411 @text_object("{") 

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

1413 """ 

1414 Move to previous blank-line separated section. 

1415 Implements '{', 'c{', 'd{', 'y{' 

1416 """ 

1417 index = event.current_buffer.document.start_of_paragraph( 

1418 count=event.arg, before=True 

1419 ) 

1420 return TextObject(index) 

1421 

1422 @text_object("}") 

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

1424 """ 

1425 Move to next blank-line separated section. 

1426 Implements '}', 'c}', 'd}', 'y}' 

1427 """ 

1428 index = event.current_buffer.document.end_of_paragraph( 

1429 count=event.arg, after=True 

1430 ) 

1431 return TextObject(index) 

1432 

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

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

1435 """ 

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

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

1438 """ 

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

1440 match = event.current_buffer.document.find( 

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

1442 ) 

1443 if match: 

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

1445 else: 

1446 return TextObject(0) 

1447 

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

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

1450 """ 

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

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

1453 """ 

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

1455 return TextObject( 

1456 event.current_buffer.document.find_backwards( 

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

1458 ) 

1459 or 0 

1460 ) 

1461 

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

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

1464 """ 

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

1466 """ 

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

1468 match = event.current_buffer.document.find( 

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

1470 ) 

1471 if match: 

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

1473 else: 

1474 return TextObject(0) 

1475 

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

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

1478 """ 

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

1480 """ 

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

1482 match = event.current_buffer.document.find_backwards( 

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

1484 ) 

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

1486 

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

1488 """ 

1489 Create ',' and ';' commands. 

1490 """ 

1491 

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

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

1494 """ 

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

1496 """ 

1497 pos: int | None = 0 

1498 vi_state = event.app.vi_state 

1499 

1500 type = TextObjectType.EXCLUSIVE 

1501 

1502 if vi_state.last_character_find: 

1503 char = vi_state.last_character_find.character 

1504 backwards = vi_state.last_character_find.backwards 

1505 

1506 if reverse: 

1507 backwards = not backwards 

1508 

1509 if backwards: 

1510 pos = event.current_buffer.document.find_backwards( 

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

1512 ) 

1513 else: 

1514 pos = event.current_buffer.document.find( 

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

1516 ) 

1517 type = TextObjectType.INCLUSIVE 

1518 if pos: 

1519 return TextObject(pos, type=type) 

1520 else: 

1521 return TextObject(0) 

1522 

1523 repeat(True) 

1524 repeat(False) 

1525 

1526 @text_object("h") 

1527 @text_object("left") 

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

1529 """ 

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

1531 """ 

1532 return TextObject( 

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

1534 ) 

1535 

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

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

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

1539 # `buffer.preferred_column`. 

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

1541 """ 

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

1543 """ 

1544 return TextObject( 

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

1546 type=TextObjectType.LINEWISE, 

1547 ) 

1548 

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

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

1551 """ 

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

1553 """ 

1554 return TextObject( 

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

1556 type=TextObjectType.LINEWISE, 

1557 ) 

1558 

1559 @text_object("l") 

1560 @text_object(" ") 

1561 @text_object("right") 

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

1563 """ 

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

1565 """ 

1566 return TextObject( 

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

1568 ) 

1569 

1570 @text_object("H") 

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

1572 """ 

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

1574 Implements 'cH', 'dH', 'H'. 

1575 """ 

1576 w = event.app.layout.current_window 

1577 b = event.current_buffer 

1578 

1579 if w and w.render_info: 

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

1581 # move to the start of the visible area. 

1582 pos = ( 

1583 b.document.translate_row_col_to_index( 

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

1585 ) 

1586 - b.cursor_position 

1587 ) 

1588 

1589 else: 

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

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

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

1593 

1594 @text_object("M") 

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

1596 """ 

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

1598 Implements 'cM', 'dM', 'M'. 

1599 """ 

1600 w = event.app.layout.current_window 

1601 b = event.current_buffer 

1602 

1603 if w and w.render_info: 

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

1605 # move to the center of the visible area. 

1606 pos = ( 

1607 b.document.translate_row_col_to_index( 

1608 w.render_info.center_visible_line(), 0 

1609 ) 

1610 - b.cursor_position 

1611 ) 

1612 

1613 else: 

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

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

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

1617 

1618 @text_object("L") 

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

1620 """ 

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

1622 """ 

1623 w = event.app.layout.current_window 

1624 b = event.current_buffer 

1625 

1626 if w and w.render_info: 

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

1628 # move to the end of the visible area. 

1629 pos = ( 

1630 b.document.translate_row_col_to_index( 

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

1632 ) 

1633 - b.cursor_position 

1634 ) 

1635 

1636 else: 

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

1638 pos = len(b.document.text_after_cursor) 

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

1640 

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

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

1643 """ 

1644 Search next. 

1645 """ 

1646 buff = event.current_buffer 

1647 search_state = event.app.current_search_state 

1648 

1649 cursor_position = buff.get_search_position( 

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

1651 ) 

1652 return TextObject(cursor_position - buff.cursor_position) 

1653 

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

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

1656 """ 

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

1658 """ 

1659 search_state = event.app.current_search_state 

1660 

1661 event.current_buffer.apply_search( 

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

1663 ) 

1664 

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

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

1667 """ 

1668 Search previous. 

1669 """ 

1670 buff = event.current_buffer 

1671 search_state = event.app.current_search_state 

1672 

1673 cursor_position = buff.get_search_position( 

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

1675 ) 

1676 return TextObject(cursor_position - buff.cursor_position) 

1677 

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

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

1680 """ 

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

1682 """ 

1683 search_state = event.app.current_search_state 

1684 

1685 event.current_buffer.apply_search( 

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

1687 ) 

1688 

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

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

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

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

1693 """ 

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

1695 """ 

1696 b = event.current_buffer 

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

1698 

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

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

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

1702 """ 

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

1704 """ 

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

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

1707 # again. 

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

1709 

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

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

1712 """ 

1713 Center Window vertically around cursor. 

1714 """ 

1715 w = event.app.layout.current_window 

1716 b = event.current_buffer 

1717 

1718 if w and w.render_info: 

1719 info = w.render_info 

1720 

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

1722 # containing the cursor in the center. 

1723 scroll_height = info.window_height // 2 

1724 

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

1726 height = 0 

1727 while y > 0: 

1728 line_height = info.get_height_for_line(y) 

1729 

1730 if height + line_height < scroll_height: 

1731 height += line_height 

1732 y -= 1 

1733 else: 

1734 break 

1735 

1736 w.vertical_scroll = y 

1737 

1738 @text_object("%") 

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

1740 """ 

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

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

1743 """ 

1744 buffer = event.current_buffer 

1745 

1746 if event._arg: 

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

1748 # row in the file. 

1749 if 0 < event.arg <= 100: 

1750 absolute_index = buffer.document.translate_row_col_to_index( 

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

1752 ) 

1753 return TextObject( 

1754 absolute_index - buffer.document.cursor_position, 

1755 type=TextObjectType.LINEWISE, 

1756 ) 

1757 else: 

1758 return TextObject(0) # Do nothing. 

1759 

1760 else: 

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

1762 match = buffer.document.find_matching_bracket_position() 

1763 if match: 

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

1765 else: 

1766 return TextObject(0) 

1767 

1768 @text_object("|") 

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

1770 """ 

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

1772 number keys, for example, 20|). 

1773 """ 

1774 return TextObject( 

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

1776 ) 

1777 

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

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

1780 """ 

1781 Go to the start of the very first line. 

1782 Implements 'gg', 'cgg', 'ygg' 

1783 """ 

1784 d = event.current_buffer.document 

1785 

1786 if event._arg: 

1787 # Move to the given line. 

1788 return TextObject( 

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

1790 type=TextObjectType.LINEWISE, 

1791 ) 

1792 else: 

1793 # Move to the top of the input. 

1794 return TextObject( 

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

1796 ) 

1797 

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

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

1800 """ 

1801 Go to last non-blank of line. 

1802 'g_', 'cg_', 'yg_', etc.. 

1803 """ 

1804 return TextObject( 

1805 event.current_buffer.document.last_non_blank_of_current_line_position(), 

1806 type=TextObjectType.INCLUSIVE, 

1807 ) 

1808 

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

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

1811 """ 

1812 Go to last character of previous word. 

1813 'ge', 'cge', 'yge', etc.. 

1814 """ 

1815 prev_end = event.current_buffer.document.find_previous_word_ending( 

1816 count=event.arg 

1817 ) 

1818 return TextObject( 

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

1820 ) 

1821 

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

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

1824 """ 

1825 Go to last character of previous WORD. 

1826 'gE', 'cgE', 'ygE', etc.. 

1827 """ 

1828 prev_end = event.current_buffer.document.find_previous_word_ending( 

1829 count=event.arg, WORD=True 

1830 ) 

1831 return TextObject( 

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

1833 ) 

1834 

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

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

1837 """ 

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

1839 """ 

1840 w = event.app.layout.current_window 

1841 buff = event.current_buffer 

1842 

1843 if w and w.render_info: 

1844 width = w.render_info.window_width 

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

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

1847 

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

1849 return TextObject(0) 

1850 

1851 @text_object("G") 

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

1853 """ 

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

1855 """ 

1856 buf = event.current_buffer 

1857 return TextObject( 

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

1859 - buf.cursor_position, 

1860 type=TextObjectType.LINEWISE, 

1861 ) 

1862 

1863 # 

1864 # *** Other *** 

1865 # 

1866 

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

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

1869 """ 

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

1871 example, 15G) 

1872 """ 

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

1874 

1875 for n in "123456789": 

1876 

1877 @handle( 

1878 n, 

1879 filter=vi_navigation_mode 

1880 | vi_selection_mode 

1881 | vi_waiting_for_text_object_mode, 

1882 ) 

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

1884 """ 

1885 Always handle numerics in navigation mode as arg. 

1886 """ 

1887 event.append_to_arg_count(event.data) 

1888 

1889 @handle( 

1890 "0", 

1891 filter=( 

1892 vi_navigation_mode | vi_selection_mode | vi_waiting_for_text_object_mode 

1893 ) 

1894 & has_arg, 

1895 ) 

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

1897 """ 

1898 Zero when an argument was already give. 

1899 """ 

1900 event.append_to_arg_count(event.data) 

1901 

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

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

1904 """ 

1905 Insert data at cursor position. 

1906 """ 

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

1908 

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

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

1911 """ 

1912 Replace single character at cursor position. 

1913 """ 

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

1915 event.current_buffer.cursor_position -= 1 

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

1917 

1918 @handle( 

1919 Keys.Any, 

1920 filter=vi_insert_multiple_mode, 

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

1922 ) 

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

1924 """ 

1925 Insert data at multiple cursor positions at once. 

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

1927 """ 

1928 buff = event.current_buffer 

1929 original_text = buff.text 

1930 

1931 # Construct new text. 

1932 text = [] 

1933 p = 0 

1934 

1935 for p2 in buff.multiple_cursor_positions: 

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

1937 text.append(event.data) 

1938 p = p2 

1939 

1940 text.append(original_text[p:]) 

1941 

1942 # Shift all cursor positions. 

1943 new_cursor_positions = [ 

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

1945 ] 

1946 

1947 # Set result. 

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

1949 buff.multiple_cursor_positions = new_cursor_positions 

1950 buff.cursor_position += 1 

1951 

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

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

1954 """ 

1955 Backspace, using multiple cursors. 

1956 """ 

1957 buff = event.current_buffer 

1958 original_text = buff.text 

1959 

1960 # Construct new text. 

1961 deleted_something = False 

1962 text = [] 

1963 p = 0 

1964 

1965 for p2 in buff.multiple_cursor_positions: 

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

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

1968 deleted_something = True 

1969 else: 

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

1971 p = p2 

1972 

1973 text.append(original_text[p:]) 

1974 

1975 if deleted_something: 

1976 # Shift all cursor positions. 

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

1978 new_cursor_positions = list(accumulate(lengths)) 

1979 

1980 # Set result. 

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

1982 buff.multiple_cursor_positions = new_cursor_positions 

1983 buff.cursor_position -= 1 

1984 else: 

1985 event.app.output.bell() 

1986 

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

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

1989 """ 

1990 Delete, using multiple cursors. 

1991 """ 

1992 buff = event.current_buffer 

1993 original_text = buff.text 

1994 

1995 # Construct new text. 

1996 deleted_something = False 

1997 text = [] 

1998 new_cursor_positions = [] 

1999 p = 0 

2000 

2001 for p2 in buff.multiple_cursor_positions: 

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

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

2004 # Don't delete across lines. 

2005 p = p2 

2006 else: 

2007 p = p2 + 1 

2008 deleted_something = True 

2009 

2010 text.append(original_text[p:]) 

2011 

2012 if deleted_something: 

2013 # Shift all cursor positions. 

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

2015 new_cursor_positions = list(accumulate(lengths)) 

2016 

2017 # Set result. 

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

2019 buff.multiple_cursor_positions = new_cursor_positions 

2020 else: 

2021 event.app.output.bell() 

2022 

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

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

2025 """ 

2026 Move all cursors to the left. 

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

2028 """ 

2029 buff = event.current_buffer 

2030 new_positions = [] 

2031 

2032 for p in buff.multiple_cursor_positions: 

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

2034 p -= 1 

2035 new_positions.append(p) 

2036 

2037 buff.multiple_cursor_positions = new_positions 

2038 

2039 if buff.document.cursor_position_col > 0: 

2040 buff.cursor_position -= 1 

2041 

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

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

2044 """ 

2045 Move all cursors to the right. 

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

2047 """ 

2048 buff = event.current_buffer 

2049 new_positions = [] 

2050 

2051 for p in buff.multiple_cursor_positions: 

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

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

2054 p += 1 

2055 new_positions.append(p) 

2056 

2057 buff.multiple_cursor_positions = new_positions 

2058 

2059 if not buff.document.is_cursor_at_the_end_of_line: 

2060 buff.cursor_position += 1 

2061 

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

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

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

2065 """ 

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

2067 """ 

2068 

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

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

2071 """ 

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

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

2074 """ 

2075 event.current_buffer.start_history_lines_completion() 

2076 

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

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

2079 """ 

2080 Complete file names. 

2081 """ 

2082 # TODO 

2083 pass 

2084 

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

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

2087 """ 

2088 Go into digraph mode. 

2089 """ 

2090 event.app.vi_state.waiting_for_digraph = True 

2091 

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

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

2094 """ 

2095 First digraph symbol. 

2096 """ 

2097 event.app.vi_state.digraph_symbol1 = event.data 

2098 

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

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

2101 """ 

2102 Insert digraph. 

2103 """ 

2104 try: 

2105 # Lookup. 

2106 code: tuple[str, str] = ( 

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

2108 event.data, 

2109 ) 

2110 if code not in DIGRAPHS: 

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

2112 symbol = DIGRAPHS[code] 

2113 except KeyError: 

2114 # Unknown digraph. 

2115 event.app.output.bell() 

2116 else: 

2117 # Insert digraph. 

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

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

2120 event.app.vi_state.waiting_for_digraph = False 

2121 finally: 

2122 event.app.vi_state.waiting_for_digraph = False 

2123 event.app.vi_state.digraph_symbol1 = None 

2124 

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

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

2127 """ 

2128 Go into normal mode for one single action. 

2129 """ 

2130 event.app.vi_state.temporary_navigation_mode = True 

2131 

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

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

2134 """ 

2135 Start recording macro. 

2136 """ 

2137 c = event.key_sequence[1].data 

2138 if c in vi_register_names: 

2139 vi_state = event.app.vi_state 

2140 

2141 vi_state.recording_register = c 

2142 vi_state.current_recording = "" 

2143 

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

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

2146 """ 

2147 Stop recording macro. 

2148 """ 

2149 vi_state = event.app.vi_state 

2150 

2151 # Store and stop recording. 

2152 if vi_state.recording_register: 

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

2154 vi_state.current_recording 

2155 ) 

2156 vi_state.recording_register = None 

2157 vi_state.current_recording = "" 

2158 

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

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

2161 """ 

2162 Execute macro. 

2163 

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

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

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

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

2168 `record_in_macro=True`. 

2169 """ 

2170 # Retrieve macro. 

2171 c = event.key_sequence[1].data 

2172 try: 

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

2174 except KeyError: 

2175 return 

2176 

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

2178 # Use vt100 parser for this. 

2179 keys: list[KeyPress] = [] 

2180 

2181 parser = Vt100Parser(keys.append) 

2182 parser.feed(macro.text) 

2183 parser.flush() 

2184 

2185 # Now feed keys back to the input processor. 

2186 for _ in range(event.arg): 

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

2188 

2189 return ConditionalKeyBindings(key_bindings, vi_mode) 

2190 

2191 

2192def load_vi_search_bindings() -> KeyBindingsBase: 

2193 key_bindings = KeyBindings() 

2194 handle = key_bindings.add 

2195 from . import search 

2196 

2197 # Vi-style forward search. 

2198 handle( 

2199 "/", 

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

2201 )(search.start_forward_incremental_search) 

2202 handle( 

2203 "?", 

2204 filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, 

2205 )(search.start_forward_incremental_search) 

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

2207 

2208 # Vi-style backward search. 

2209 handle( 

2210 "?", 

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

2212 )(search.start_reverse_incremental_search) 

2213 handle( 

2214 "/", 

2215 filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, 

2216 )(search.start_reverse_incremental_search) 

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

2218 

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

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

2221 

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

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

2224 

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

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

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

2228 

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

2230 # `abort_search` would be a meaningful alternative. 

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

2232 

2233 return ConditionalKeyBindings(key_bindings, vi_mode)