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

271 statements  

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

1# pylint: disable=function-redefined 

2from __future__ import annotations 

3 

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 

36def load_emacs_bindings() -> KeyBindingsBase: 

37 """ 

38 Some e-macs extensions. 

39 """ 

40 # Overview of Readline emacs commands: 

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

42 key_bindings = KeyBindings() 

43 handle = key_bindings.add 

44 

45 insert_mode = emacs_insert_mode 

46 

47 @handle("escape") 

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

49 """ 

50 By default, ignore escape key. 

51 

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

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

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

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

56 """ 

57 pass 

58 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

77 

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

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

80 

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

82 get_by_name("undo") 

83 ) 

84 

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

86 get_by_name("undo") 

87 ) 

88 

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

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

91 

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

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

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

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

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

97 

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

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

100 # Ctrl-S are captured by the terminal. 

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

102 

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

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

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

106 

107 @handle("c-n") 

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

109 "Next line." 

110 event.current_buffer.auto_down() 

111 

112 @handle("c-p") 

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

114 "Previous line." 

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

116 

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

118 """ 

119 Handle input of arguments. 

120 The first number needs to be preceded by escape. 

121 """ 

122 

123 @handle(c, filter=has_arg) 

124 @handle("escape", c) 

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

126 event.append_to_arg_count(c) 

127 

128 for c in "0123456789": 

129 handle_digit(c) 

130 

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

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

133 """""" 

134 if event._arg is None: 

135 event.append_to_arg_count("-") 

136 

137 @handle("-", filter=Condition(lambda: get_app().key_processor.arg == "-")) 

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

139 """ 

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

141 argument, ignore this. 

142 """ 

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

144 

145 @Condition 

146 def is_returnable() -> bool: 

147 return get_app().current_buffer.is_returnable 

148 

149 # Meta + Enter: always accept input. 

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

151 get_by_name("accept-line") 

152 ) 

153 

154 # Enter: accept input in single line mode. 

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

156 get_by_name("accept-line") 

157 ) 

158 

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

160 if count < 0: 

161 match = buff.document.find_backwards( 

162 char, in_current_line=True, count=-count 

163 ) 

164 else: 

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

166 

167 if match is not None: 

168 buff.cursor_position += match 

169 

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

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

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

173 # Also named 'character-search' 

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

175 

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

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

178 "Like Ctl-], but backwards." 

179 # Also named 'character-search-backward' 

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

181 

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

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

184 "Previous sentence." 

185 # TODO: 

186 

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

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

189 "Move to end of sentence." 

190 # TODO: 

191 

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

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

194 """ 

195 Swap the last two words before the cursor. 

196 """ 

197 # TODO 

198 

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

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

201 """ 

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

203 """ 

204 buff = event.current_buffer 

205 

206 # List all completions. 

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

208 completions = list( 

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

210 ) 

211 

212 # Insert them. 

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

214 buff.insert_text(text_to_insert) 

215 

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

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

218 """ 

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

220 line. 

221 """ 

222 buffer = event.current_buffer 

223 

224 if buffer.document.is_cursor_at_the_end_of_line: 

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

226 after_whitespace=False 

227 ) 

228 else: 

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

230 

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

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

233 """ 

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

235 """ 

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

237 buff = event.current_buffer 

238 if buff.text: 

239 buff.start_selection(selection_type=SelectionType.CHARACTERS) 

240 

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

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

243 """ 

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

245 """ 

246 event.current_buffer.complete_state = None 

247 event.current_buffer.validation_error = None 

248 

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

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

251 """ 

252 Cancel selection. 

253 """ 

254 event.current_buffer.exit_selection() 

255 

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

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

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

259 """ 

260 Cut selected text. 

261 """ 

262 data = event.current_buffer.cut_selection() 

263 event.app.clipboard.set_data(data) 

264 

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

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

267 """ 

268 Copy selected text. 

269 """ 

270 data = event.current_buffer.copy_selection() 

271 event.app.clipboard.set_data(data) 

272 

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

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

275 """ 

276 Cursor to start of previous word. 

277 """ 

278 buffer = event.current_buffer 

279 buffer.cursor_position += ( 

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

281 ) 

282 

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

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

285 """ 

286 Cursor to start of next word. 

287 """ 

288 buffer = event.current_buffer 

289 buffer.cursor_position += ( 

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

291 or buffer.document.get_end_of_document_position() 

292 ) 

293 

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

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

296 """ 

297 M-/: Complete. 

298 """ 

299 b = event.current_buffer 

300 if b.complete_state: 

301 b.complete_next() 

302 else: 

303 b.start_completion(select_first=True) 

304 

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

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

307 """ 

308 Indent selected text. 

309 """ 

310 buffer = event.current_buffer 

311 

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

313 after_whitespace=True 

314 ) 

315 

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

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

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

319 

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

321 

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

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

324 """ 

325 Unindent selected text. 

326 """ 

327 buffer = event.current_buffer 

328 

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

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

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

332 

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

334 

335 return ConditionalKeyBindings(key_bindings, emacs_mode) 

336 

337 

338def load_emacs_search_bindings() -> KeyBindingsBase: 

339 key_bindings = KeyBindings() 

340 handle = key_bindings.add 

341 from . import search 

342 

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

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

345 # Instead, we have double escape. 

346 

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

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

349 

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

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

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

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

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

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

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

357 

358 # Handling of escape. 

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

360 

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

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

363 # instead. 

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

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

366 

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

368 

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

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

371 search.start_reverse_incremental_search 

372 ) 

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

374 search.start_forward_incremental_search 

375 ) 

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

377 search.start_forward_incremental_search 

378 ) 

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

380 search.start_reverse_incremental_search 

381 ) 

382 

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

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

385 "Jump to next match." 

386 event.current_buffer.apply_search( 

387 event.app.current_search_state, 

388 include_current_position=False, 

389 count=event.arg, 

390 ) 

391 

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

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

394 "Jump to previous match." 

395 event.current_buffer.apply_search( 

396 ~event.app.current_search_state, 

397 include_current_position=False, 

398 count=event.arg, 

399 ) 

400 

401 return ConditionalKeyBindings(key_bindings, emacs_mode) 

402 

403 

404def load_emacs_shift_selection_bindings() -> KeyBindingsBase: 

405 """ 

406 Bindings to select text with shift + cursor movements 

407 """ 

408 

409 key_bindings = KeyBindings() 

410 handle = key_bindings.add 

411 

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

413 """ 

414 Used for the shift selection mode. When called with 

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

416 as if shift is not pressed. 

417 """ 

418 key = event.key_sequence[0].key 

419 

420 if key == Keys.ShiftUp: 

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

422 return 

423 if key == Keys.ShiftDown: 

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

425 return 

426 

427 # the other keys are handled through their readline command 

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

429 Keys.ShiftLeft: "backward-char", 

430 Keys.ShiftRight: "forward-char", 

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

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

433 Keys.ControlShiftLeft: "backward-word", 

434 Keys.ControlShiftRight: "forward-word", 

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

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

437 } 

438 

439 try: 

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

441 binding = get_by_name(key_to_command[key]) 

442 except KeyError: 

443 pass 

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

445 if isinstance(binding, Binding): 

446 # (It should always be a binding here) 

447 binding.call(event) 

448 

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

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

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

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

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

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

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

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

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

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

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

460 """ 

461 Start selection with shift + movement. 

462 """ 

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

464 buff = event.current_buffer 

465 if buff.text: 

466 buff.start_selection(selection_type=SelectionType.CHARACTERS) 

467 

468 if buff.selection_state is not None: 

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

470 # `start_selection`.) 

471 buff.selection_state.enter_shift_mode() 

472 

473 # Then move the cursor 

474 original_position = buff.cursor_position 

475 unshift_move(event) 

476 if buff.cursor_position == original_position: 

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

478 # to avoid having an empty selection 

479 buff.exit_selection() 

480 

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

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

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

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

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

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

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

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

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

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

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

492 """ 

493 Extend the selection 

494 """ 

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

496 unshift_move(event) 

497 buff = event.current_buffer 

498 

499 if buff.selection_state is not None: 

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

501 # selection is now empty, so cancel selection 

502 buff.exit_selection() 

503 

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

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

506 """ 

507 Replace selection by what is typed 

508 """ 

509 event.current_buffer.cut_selection() 

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

511 

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

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

514 """ 

515 A newline replaces the selection 

516 """ 

517 event.current_buffer.cut_selection() 

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

519 

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

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

522 """ 

523 Delete selection. 

524 """ 

525 event.current_buffer.cut_selection() 

526 

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

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

529 """ 

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

531 """ 

532 buff = event.current_buffer 

533 if buff.selection_state: 

534 buff.cut_selection() 

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

536 

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

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

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

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

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

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

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

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

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

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

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

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

549 """ 

550 Cancel selection. 

551 """ 

552 event.current_buffer.exit_selection() 

553 # we then process the cursor movement 

554 key_press = event.key_sequence[0] 

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

556 

557 return ConditionalKeyBindings(key_bindings, emacs_mode)