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

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

275 statements  

1# pylint: disable=function-redefined 

2from __future__ import annotations 

3 

4from prompt_toolkit.application.current import get_app 

5from prompt_toolkit.buffer import Buffer, indent, unindent 

6from prompt_toolkit.completion import CompleteEvent 

7from prompt_toolkit.filters import ( 

8 Condition, 

9 emacs_insert_mode, 

10 emacs_mode, 

11 has_arg, 

12 has_selection, 

13 in_paste_mode, 

14 is_multiline, 

15 is_read_only, 

16 shift_selection_mode, 

17 vi_search_direction_reversed, 

18) 

19from prompt_toolkit.key_binding.key_bindings import Binding 

20from prompt_toolkit.key_binding.key_processor import KeyPressEvent 

21from prompt_toolkit.keys import Keys 

22from prompt_toolkit.selection import SelectionType 

23 

24from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase 

25from .named_commands import get_by_name 

26 

27__all__ = [ 

28 "load_emacs_bindings", 

29 "load_emacs_search_bindings", 

30 "load_emacs_shift_selection_bindings", 

31] 

32 

33E = KeyPressEvent 

34 

35 

36@Condition 

37def is_returnable() -> bool: 

38 return get_app().current_buffer.is_returnable 

39 

40 

41@Condition 

42def is_arg() -> bool: 

43 return get_app().key_processor.arg == "-" 

44 

45 

46def load_emacs_bindings() -> KeyBindingsBase: 

47 """ 

48 Some e-macs extensions. 

49 """ 

50 # Overview of Readline emacs commands: 

51 # http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf 

52 key_bindings = KeyBindings() 

53 handle = key_bindings.add 

54 

55 insert_mode = emacs_insert_mode 

56 

57 @handle("escape") 

58 def _esc(event: E) -> None: 

59 """ 

60 By default, ignore escape key. 

61 

62 (If we don't put this here, and Esc is followed by a key which sequence 

63 is not handled, we'll insert an Escape character in the input stream. 

64 Something we don't want and happens to easily in emacs mode. 

65 Further, people can always use ControlQ to do a quoted insert.) 

66 """ 

67 pass 

68 

69 handle("c-a")(get_by_name("beginning-of-line")) 

70 handle("c-b")(get_by_name("backward-char")) 

71 handle("c-delete", filter=insert_mode)(get_by_name("kill-word")) 

72 handle("c-e")(get_by_name("end-of-line")) 

73 handle("c-f")(get_by_name("forward-char")) 

74 handle("c-left")(get_by_name("backward-word")) 

75 handle("c-right")(get_by_name("forward-word")) 

76 handle("c-x", "r", "y", filter=insert_mode)(get_by_name("yank")) 

77 handle("c-y", filter=insert_mode)(get_by_name("yank")) 

78 handle("escape", "b")(get_by_name("backward-word")) 

79 handle("escape", "c", filter=insert_mode)(get_by_name("capitalize-word")) 

80 handle("escape", "d", filter=insert_mode)(get_by_name("kill-word")) 

81 handle("escape", "f")(get_by_name("forward-word")) 

82 handle("escape", "l", filter=insert_mode)(get_by_name("downcase-word")) 

83 handle("escape", "u", filter=insert_mode)(get_by_name("uppercase-word")) 

84 handle("escape", "y", filter=insert_mode)(get_by_name("yank-pop")) 

85 handle("escape", "backspace", filter=insert_mode)(get_by_name("backward-kill-word")) 

86 handle("escape", "\\", filter=insert_mode)(get_by_name("delete-horizontal-space")) 

87 

88 handle("c-home")(get_by_name("beginning-of-buffer")) 

89 handle("c-end")(get_by_name("end-of-buffer")) 

90 

91 handle("c-_", save_before=(lambda e: False), filter=insert_mode)( 

92 get_by_name("undo") 

93 ) 

94 

95 handle("c-x", "c-u", save_before=(lambda e: False), filter=insert_mode)( 

96 get_by_name("undo") 

97 ) 

98 

99 handle("escape", "<", filter=~has_selection)(get_by_name("beginning-of-history")) 

100 handle("escape", ">", filter=~has_selection)(get_by_name("end-of-history")) 

101 

102 handle("escape", ".", filter=insert_mode)(get_by_name("yank-last-arg")) 

103 handle("escape", "_", filter=insert_mode)(get_by_name("yank-last-arg")) 

104 handle("escape", "c-y", filter=insert_mode)(get_by_name("yank-nth-arg")) 

105 handle("escape", "#", filter=insert_mode)(get_by_name("insert-comment")) 

106 handle("c-o")(get_by_name("operate-and-get-next")) 

107 

108 # ControlQ does a quoted insert. Not that for vt100 terminals, you have to 

109 # disable flow control by running ``stty -ixon``, otherwise Ctrl-Q and 

110 # Ctrl-S are captured by the terminal. 

111 handle("c-q", filter=~has_selection)(get_by_name("quoted-insert")) 

112 

113 handle("c-x", "(")(get_by_name("start-kbd-macro")) 

114 handle("c-x", ")")(get_by_name("end-kbd-macro")) 

115 handle("c-x", "e")(get_by_name("call-last-kbd-macro")) 

116 

117 @handle("c-n") 

118 def _next(event: E) -> None: 

119 "Next line." 

120 event.current_buffer.auto_down() 

121 

122 @handle("c-p") 

123 def _prev(event: E) -> None: 

124 "Previous line." 

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

126 

127 def handle_digit(c: str) -> None: 

128 """ 

129 Handle input of arguments. 

130 The first number needs to be preceded by escape. 

131 """ 

132 

133 @handle(c, filter=has_arg) 

134 @handle("escape", c) 

135 def _(event: E) -> None: 

136 event.append_to_arg_count(c) 

137 

138 for c in "0123456789": 

139 handle_digit(c) 

140 

141 @handle("escape", "-", filter=~has_arg) 

142 def _meta_dash(event: E) -> None: 

143 """""" 

144 if event._arg is None: 

145 event.append_to_arg_count("-") 

146 

147 @handle("-", filter=is_arg) 

148 def _dash(event: E) -> None: 

149 """ 

150 When '-' is typed again, after exactly '-' has been given as an 

151 argument, ignore this. 

152 """ 

153 event.app.key_processor.arg = "-" 

154 

155 # Meta + Enter: always accept input. 

156 handle("escape", "enter", filter=insert_mode & is_returnable)( 

157 get_by_name("accept-line") 

158 ) 

159 

160 # Enter: accept input in single line mode. 

161 handle("enter", filter=insert_mode & is_returnable & ~is_multiline)( 

162 get_by_name("accept-line") 

163 ) 

164 

165 def character_search(buff: Buffer, char: str, count: int) -> None: 

166 if count < 0: 

167 match = buff.document.find_backwards( 

168 char, in_current_line=True, count=-count 

169 ) 

170 else: 

171 match = buff.document.find(char, in_current_line=True, count=count) 

172 

173 if match is not None: 

174 buff.cursor_position += match 

175 

176 @handle("c-]", Keys.Any) 

177 def _goto_char(event: E) -> None: 

178 "When Ctl-] + a character is pressed. go to that character." 

179 # Also named 'character-search' 

180 character_search(event.current_buffer, event.data, event.arg) 

181 

182 @handle("escape", "c-]", Keys.Any) 

183 def _goto_char_backwards(event: E) -> None: 

184 "Like Ctl-], but backwards." 

185 # Also named 'character-search-backward' 

186 character_search(event.current_buffer, event.data, -event.arg) 

187 

188 @handle("escape", "a") 

189 def _prev_sentence(event: E) -> None: 

190 "Previous sentence." 

191 # TODO: 

192 

193 @handle("escape", "e") 

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

195 "Move to end of sentence." 

196 # TODO: 

197 

198 @handle("escape", "t", filter=insert_mode) 

199 def _swap_characters(event: E) -> None: 

200 """ 

201 Swap the last two words before the cursor. 

202 """ 

203 # TODO 

204 

205 @handle("escape", "*", filter=insert_mode) 

206 def _insert_all_completions(event: E) -> None: 

207 """ 

208 `meta-*`: Insert all possible completions of the preceding text. 

209 """ 

210 buff = event.current_buffer 

211 

212 # List all completions. 

213 complete_event = CompleteEvent(text_inserted=False, completion_requested=True) 

214 completions = list( 

215 buff.completer.get_completions(buff.document, complete_event) 

216 ) 

217 

218 # Insert them. 

219 text_to_insert = " ".join(c.text for c in completions) 

220 buff.insert_text(text_to_insert) 

221 

222 @handle("c-x", "c-x") 

223 def _toggle_start_end(event: E) -> None: 

224 """ 

225 Move cursor back and forth between the start and end of the current 

226 line. 

227 """ 

228 buffer = event.current_buffer 

229 

230 if buffer.document.is_cursor_at_the_end_of_line: 

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

232 after_whitespace=False 

233 ) 

234 else: 

235 buffer.cursor_position += buffer.document.get_end_of_line_position() 

236 

237 @handle("c-@") # Control-space or Control-@ 

238 def _start_selection(event: E) -> None: 

239 """ 

240 Start of the selection (if the current buffer is not empty). 

241 """ 

242 # Take the current cursor position as the start of this selection. 

243 buff = event.current_buffer 

244 if buff.text: 

245 buff.start_selection(selection_type=SelectionType.CHARACTERS) 

246 

247 @handle("c-g", filter=~has_selection) 

248 def _cancel(event: E) -> None: 

249 """ 

250 Control + G: Cancel completion menu and validation state. 

251 """ 

252 event.current_buffer.complete_state = None 

253 event.current_buffer.validation_error = None 

254 

255 @handle("c-g", filter=has_selection) 

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

257 """ 

258 Cancel selection. 

259 """ 

260 event.current_buffer.exit_selection() 

261 

262 @handle("c-w", filter=has_selection) 

263 @handle("c-x", "r", "k", filter=has_selection) 

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

265 """ 

266 Cut selected text. 

267 """ 

268 data = event.current_buffer.cut_selection() 

269 event.app.clipboard.set_data(data) 

270 

271 @handle("escape", "w", filter=has_selection) 

272 def _copy(event: E) -> None: 

273 """ 

274 Copy selected text. 

275 """ 

276 data = event.current_buffer.copy_selection() 

277 event.app.clipboard.set_data(data) 

278 

279 @handle("escape", "left") 

280 def _start_of_word(event: E) -> None: 

281 """ 

282 Cursor to start of previous word. 

283 """ 

284 buffer = event.current_buffer 

285 buffer.cursor_position += ( 

286 buffer.document.find_previous_word_beginning(count=event.arg) or 0 

287 ) 

288 

289 @handle("escape", "right") 

290 def _start_next_word(event: E) -> None: 

291 """ 

292 Cursor to start of next word. 

293 """ 

294 buffer = event.current_buffer 

295 buffer.cursor_position += ( 

296 buffer.document.find_next_word_beginning(count=event.arg) 

297 or buffer.document.get_end_of_document_position() 

298 ) 

299 

300 @handle("escape", "/", filter=insert_mode) 

301 def _complete(event: E) -> None: 

302 """ 

303 M-/: Complete. 

304 """ 

305 b = event.current_buffer 

306 if b.complete_state: 

307 b.complete_next() 

308 else: 

309 b.start_completion(select_first=True) 

310 

311 @handle("c-c", ">", filter=has_selection) 

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

313 """ 

314 Indent selected text. 

315 """ 

316 buffer = event.current_buffer 

317 

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

319 after_whitespace=True 

320 ) 

321 

322 from_, to = buffer.document.selection_range() 

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

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

325 

326 indent(buffer, from_, to + 1, count=event.arg) 

327 

328 @handle("c-c", "<", filter=has_selection) 

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

330 """ 

331 Unindent selected text. 

332 """ 

333 buffer = event.current_buffer 

334 

335 from_, to = buffer.document.selection_range() 

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

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

338 

339 unindent(buffer, from_, to + 1, count=event.arg) 

340 

341 return ConditionalKeyBindings(key_bindings, emacs_mode) 

342 

343 

344def load_emacs_search_bindings() -> KeyBindingsBase: 

345 key_bindings = KeyBindings() 

346 handle = key_bindings.add 

347 from . import search 

348 

349 # NOTE: We don't bind 'Escape' to 'abort_search'. The reason is that we 

350 # want Alt+Enter to accept input directly in incremental search mode. 

351 # Instead, we have double escape. 

352 

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

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

355 

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

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

358 handle("c-r")(search.reverse_incremental_search) 

359 handle("c-s")(search.forward_incremental_search) 

360 handle("up")(search.reverse_incremental_search) 

361 handle("down")(search.forward_incremental_search) 

362 handle("enter")(search.accept_search) 

363 

364 # Handling of escape. 

365 handle("escape", eager=True)(search.accept_search) 

366 

367 # Like Readline, it's more natural to accept the search when escape has 

368 # been pressed, however instead the following two bindings could be used 

369 # instead. 

370 # #handle('escape', 'escape', eager=True)(search.abort_search) 

371 # #handle('escape', 'enter', eager=True)(search.accept_search_and_accept_input) 

372 

373 # If Read-only: also include the following key bindings: 

374 

375 # '/' and '?' key bindings for searching, just like Vi mode. 

376 handle("?", filter=is_read_only & ~vi_search_direction_reversed)( 

377 search.start_reverse_incremental_search 

378 ) 

379 handle("/", filter=is_read_only & ~vi_search_direction_reversed)( 

380 search.start_forward_incremental_search 

381 ) 

382 handle("?", filter=is_read_only & vi_search_direction_reversed)( 

383 search.start_forward_incremental_search 

384 ) 

385 handle("/", filter=is_read_only & vi_search_direction_reversed)( 

386 search.start_reverse_incremental_search 

387 ) 

388 

389 @handle("n", filter=is_read_only) 

390 def _jump_next(event: E) -> None: 

391 "Jump to next match." 

392 event.current_buffer.apply_search( 

393 event.app.current_search_state, 

394 include_current_position=False, 

395 count=event.arg, 

396 ) 

397 

398 @handle("N", filter=is_read_only) 

399 def _jump_prev(event: E) -> None: 

400 "Jump to previous match." 

401 event.current_buffer.apply_search( 

402 ~event.app.current_search_state, 

403 include_current_position=False, 

404 count=event.arg, 

405 ) 

406 

407 return ConditionalKeyBindings(key_bindings, emacs_mode) 

408 

409 

410def load_emacs_shift_selection_bindings() -> KeyBindingsBase: 

411 """ 

412 Bindings to select text with shift + cursor movements 

413 """ 

414 

415 key_bindings = KeyBindings() 

416 handle = key_bindings.add 

417 

418 def unshift_move(event: E) -> None: 

419 """ 

420 Used for the shift selection mode. When called with 

421 a shift + movement key press event, moves the cursor 

422 as if shift is not pressed. 

423 """ 

424 key = event.key_sequence[0].key 

425 

426 if key == Keys.ShiftUp: 

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

428 return 

429 if key == Keys.ShiftDown: 

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

431 return 

432 

433 # the other keys are handled through their readline command 

434 key_to_command: dict[Keys | str, str] = { 

435 Keys.ShiftLeft: "backward-char", 

436 Keys.ShiftRight: "forward-char", 

437 Keys.ShiftHome: "beginning-of-line", 

438 Keys.ShiftEnd: "end-of-line", 

439 Keys.ControlShiftLeft: "backward-word", 

440 Keys.ControlShiftRight: "forward-word", 

441 Keys.ControlShiftHome: "beginning-of-buffer", 

442 Keys.ControlShiftEnd: "end-of-buffer", 

443 } 

444 

445 try: 

446 # Both the dict lookup and `get_by_name` can raise KeyError. 

447 binding = get_by_name(key_to_command[key]) 

448 except KeyError: 

449 pass 

450 else: # (`else` is not really needed here.) 

451 if isinstance(binding, Binding): 

452 # (It should always be a binding here) 

453 binding.call(event) 

454 

455 @handle("s-left", filter=~has_selection) 

456 @handle("s-right", filter=~has_selection) 

457 @handle("s-up", filter=~has_selection) 

458 @handle("s-down", filter=~has_selection) 

459 @handle("s-home", filter=~has_selection) 

460 @handle("s-end", filter=~has_selection) 

461 @handle("c-s-left", filter=~has_selection) 

462 @handle("c-s-right", filter=~has_selection) 

463 @handle("c-s-home", filter=~has_selection) 

464 @handle("c-s-end", filter=~has_selection) 

465 def _start_selection(event: E) -> None: 

466 """ 

467 Start selection with shift + movement. 

468 """ 

469 # Take the current cursor position as the start of this selection. 

470 buff = event.current_buffer 

471 if buff.text: 

472 buff.start_selection(selection_type=SelectionType.CHARACTERS) 

473 

474 if buff.selection_state is not None: 

475 # (`selection_state` should never be `None`, it is created by 

476 # `start_selection`.) 

477 buff.selection_state.enter_shift_mode() 

478 

479 # Then move the cursor 

480 original_position = buff.cursor_position 

481 unshift_move(event) 

482 if buff.cursor_position == original_position: 

483 # Cursor didn't actually move - so cancel selection 

484 # to avoid having an empty selection 

485 buff.exit_selection() 

486 

487 @handle("s-left", filter=shift_selection_mode) 

488 @handle("s-right", filter=shift_selection_mode) 

489 @handle("s-up", filter=shift_selection_mode) 

490 @handle("s-down", filter=shift_selection_mode) 

491 @handle("s-home", filter=shift_selection_mode) 

492 @handle("s-end", filter=shift_selection_mode) 

493 @handle("c-s-left", filter=shift_selection_mode) 

494 @handle("c-s-right", filter=shift_selection_mode) 

495 @handle("c-s-home", filter=shift_selection_mode) 

496 @handle("c-s-end", filter=shift_selection_mode) 

497 def _extend_selection(event: E) -> None: 

498 """ 

499 Extend the selection 

500 """ 

501 # Just move the cursor, like shift was not pressed 

502 unshift_move(event) 

503 buff = event.current_buffer 

504 

505 if buff.selection_state is not None: 

506 if buff.cursor_position == buff.selection_state.original_cursor_position: 

507 # selection is now empty, so cancel selection 

508 buff.exit_selection() 

509 

510 @handle(Keys.Any, filter=shift_selection_mode) 

511 def _replace_selection(event: E) -> None: 

512 """ 

513 Replace selection by what is typed 

514 """ 

515 event.current_buffer.cut_selection() 

516 get_by_name("self-insert").call(event) 

517 

518 @handle("enter", filter=shift_selection_mode & is_multiline) 

519 def _newline(event: E) -> None: 

520 """ 

521 A newline replaces the selection 

522 """ 

523 event.current_buffer.cut_selection() 

524 event.current_buffer.newline(copy_margin=not in_paste_mode()) 

525 

526 @handle("backspace", filter=shift_selection_mode) 

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

528 """ 

529 Delete selection. 

530 """ 

531 event.current_buffer.cut_selection() 

532 

533 @handle("c-y", filter=shift_selection_mode) 

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

535 """ 

536 In shift selection mode, yanking (pasting) replace the selection. 

537 """ 

538 buff = event.current_buffer 

539 if buff.selection_state: 

540 buff.cut_selection() 

541 get_by_name("yank").call(event) 

542 

543 # moving the cursor in shift selection mode cancels the selection 

544 @handle("left", filter=shift_selection_mode) 

545 @handle("right", filter=shift_selection_mode) 

546 @handle("up", filter=shift_selection_mode) 

547 @handle("down", filter=shift_selection_mode) 

548 @handle("home", filter=shift_selection_mode) 

549 @handle("end", filter=shift_selection_mode) 

550 @handle("c-left", filter=shift_selection_mode) 

551 @handle("c-right", filter=shift_selection_mode) 

552 @handle("c-home", filter=shift_selection_mode) 

553 @handle("c-end", filter=shift_selection_mode) 

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

555 """ 

556 Cancel selection. 

557 """ 

558 event.current_buffer.exit_selection() 

559 # we then process the cursor movement 

560 key_press = event.key_sequence[0] 

561 event.key_processor.feed(key_press, first=True) 

562 

563 return ConditionalKeyBindings(key_bindings, emacs_mode)