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

272 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:07 +0000

1# pylint: disable=function-redefined 

2from __future__ import annotations 

3 

4from typing import Dict, Union 

5 

6from prompt_toolkit.application.current import get_app 

7from prompt_toolkit.buffer import Buffer, indent, unindent 

8from prompt_toolkit.completion import CompleteEvent 

9from prompt_toolkit.filters import ( 

10 Condition, 

11 emacs_insert_mode, 

12 emacs_mode, 

13 has_arg, 

14 has_selection, 

15 in_paste_mode, 

16 is_multiline, 

17 is_read_only, 

18 shift_selection_mode, 

19 vi_search_direction_reversed, 

20) 

21from prompt_toolkit.key_binding.key_bindings import Binding 

22from prompt_toolkit.key_binding.key_processor import KeyPressEvent 

23from prompt_toolkit.keys import Keys 

24from prompt_toolkit.selection import SelectionType 

25 

26from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase 

27from .named_commands import get_by_name 

28 

29__all__ = [ 

30 "load_emacs_bindings", 

31 "load_emacs_search_bindings", 

32 "load_emacs_shift_selection_bindings", 

33] 

34 

35E = KeyPressEvent 

36 

37 

38def load_emacs_bindings() -> KeyBindingsBase: 

39 """ 

40 Some e-macs extensions. 

41 """ 

42 # Overview of Readline emacs commands: 

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

44 key_bindings = KeyBindings() 

45 handle = key_bindings.add 

46 

47 insert_mode = emacs_insert_mode 

48 

49 @handle("escape") 

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

51 """ 

52 By default, ignore escape key. 

53 

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

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

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

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

58 """ 

59 pass 

60 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

79 

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

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

82 

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

84 get_by_name("undo") 

85 ) 

86 

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

88 get_by_name("undo") 

89 ) 

90 

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

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

93 

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

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

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

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

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

99 

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

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

102 # Ctrl-S are captured by the terminal. 

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

104 

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

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

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

108 

109 @handle("c-n") 

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

111 "Next line." 

112 event.current_buffer.auto_down() 

113 

114 @handle("c-p") 

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

116 "Previous line." 

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

118 

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

120 """ 

121 Handle input of arguments. 

122 The first number needs to be preceded by escape. 

123 """ 

124 

125 @handle(c, filter=has_arg) 

126 @handle("escape", c) 

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

128 event.append_to_arg_count(c) 

129 

130 for c in "0123456789": 

131 handle_digit(c) 

132 

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

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

135 """""" 

136 if event._arg is None: 

137 event.append_to_arg_count("-") 

138 

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

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

141 """ 

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

143 argument, ignore this. 

144 """ 

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

146 

147 @Condition 

148 def is_returnable() -> bool: 

149 return get_app().current_buffer.is_returnable 

150 

151 # Meta + Enter: always accept input. 

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

153 get_by_name("accept-line") 

154 ) 

155 

156 # Enter: accept input in single line mode. 

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

158 get_by_name("accept-line") 

159 ) 

160 

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

162 if count < 0: 

163 match = buff.document.find_backwards( 

164 char, in_current_line=True, count=-count 

165 ) 

166 else: 

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

168 

169 if match is not None: 

170 buff.cursor_position += match 

171 

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

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

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

175 # Also named 'character-search' 

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

177 

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

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

180 "Like Ctl-], but backwards." 

181 # Also named 'character-search-backward' 

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

183 

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

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

186 "Previous sentence." 

187 # TODO: 

188 

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

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

191 "Move to end of sentence." 

192 # TODO: 

193 

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

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

196 """ 

197 Swap the last two words before the cursor. 

198 """ 

199 # TODO 

200 

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

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

203 """ 

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

205 """ 

206 buff = event.current_buffer 

207 

208 # List all completions. 

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

210 completions = list( 

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

212 ) 

213 

214 # Insert them. 

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

216 buff.insert_text(text_to_insert) 

217 

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

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

220 """ 

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

222 line. 

223 """ 

224 buffer = event.current_buffer 

225 

226 if buffer.document.is_cursor_at_the_end_of_line: 

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

228 after_whitespace=False 

229 ) 

230 else: 

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

232 

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

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

235 """ 

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

237 """ 

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

239 buff = event.current_buffer 

240 if buff.text: 

241 buff.start_selection(selection_type=SelectionType.CHARACTERS) 

242 

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

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

245 """ 

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

247 """ 

248 event.current_buffer.complete_state = None 

249 event.current_buffer.validation_error = None 

250 

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

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

253 """ 

254 Cancel selection. 

255 """ 

256 event.current_buffer.exit_selection() 

257 

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

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

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

261 """ 

262 Cut selected text. 

263 """ 

264 data = event.current_buffer.cut_selection() 

265 event.app.clipboard.set_data(data) 

266 

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

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

269 """ 

270 Copy selected text. 

271 """ 

272 data = event.current_buffer.copy_selection() 

273 event.app.clipboard.set_data(data) 

274 

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

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

277 """ 

278 Cursor to start of previous word. 

279 """ 

280 buffer = event.current_buffer 

281 buffer.cursor_position += ( 

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

283 ) 

284 

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

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

287 """ 

288 Cursor to start of next word. 

289 """ 

290 buffer = event.current_buffer 

291 buffer.cursor_position += ( 

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

293 or buffer.document.get_end_of_document_position() 

294 ) 

295 

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

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

298 """ 

299 M-/: Complete. 

300 """ 

301 b = event.current_buffer 

302 if b.complete_state: 

303 b.complete_next() 

304 else: 

305 b.start_completion(select_first=True) 

306 

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

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

309 """ 

310 Indent selected text. 

311 """ 

312 buffer = event.current_buffer 

313 

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

315 after_whitespace=True 

316 ) 

317 

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

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

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

321 

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

323 

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

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

326 """ 

327 Unindent selected text. 

328 """ 

329 buffer = event.current_buffer 

330 

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

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

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

334 

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

336 

337 return ConditionalKeyBindings(key_bindings, emacs_mode) 

338 

339 

340def load_emacs_search_bindings() -> KeyBindingsBase: 

341 key_bindings = KeyBindings() 

342 handle = key_bindings.add 

343 from . import search 

344 

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

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

347 # Instead, we have double escape. 

348 

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

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

351 

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

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

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

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

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

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

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

359 

360 # Handling of escape. 

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

362 

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

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

365 # instead. 

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

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

368 

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

370 

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

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

373 search.start_reverse_incremental_search 

374 ) 

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

376 search.start_forward_incremental_search 

377 ) 

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

379 search.start_forward_incremental_search 

380 ) 

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

382 search.start_reverse_incremental_search 

383 ) 

384 

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

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

387 "Jump to next match." 

388 event.current_buffer.apply_search( 

389 event.app.current_search_state, 

390 include_current_position=False, 

391 count=event.arg, 

392 ) 

393 

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

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

396 "Jump to previous match." 

397 event.current_buffer.apply_search( 

398 ~event.app.current_search_state, 

399 include_current_position=False, 

400 count=event.arg, 

401 ) 

402 

403 return ConditionalKeyBindings(key_bindings, emacs_mode) 

404 

405 

406def load_emacs_shift_selection_bindings() -> KeyBindingsBase: 

407 """ 

408 Bindings to select text with shift + cursor movements 

409 """ 

410 

411 key_bindings = KeyBindings() 

412 handle = key_bindings.add 

413 

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

415 """ 

416 Used for the shift selection mode. When called with 

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

418 as if shift is not pressed. 

419 """ 

420 key = event.key_sequence[0].key 

421 

422 if key == Keys.ShiftUp: 

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

424 return 

425 if key == Keys.ShiftDown: 

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

427 return 

428 

429 # the other keys are handled through their readline command 

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

431 Keys.ShiftLeft: "backward-char", 

432 Keys.ShiftRight: "forward-char", 

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

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

435 Keys.ControlShiftLeft: "backward-word", 

436 Keys.ControlShiftRight: "forward-word", 

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

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

439 } 

440 

441 try: 

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

443 binding = get_by_name(key_to_command[key]) 

444 except KeyError: 

445 pass 

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

447 if isinstance(binding, Binding): 

448 # (It should always be a binding here) 

449 binding.call(event) 

450 

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

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

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

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

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

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

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

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

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

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

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

462 """ 

463 Start selection with shift + movement. 

464 """ 

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

466 buff = event.current_buffer 

467 if buff.text: 

468 buff.start_selection(selection_type=SelectionType.CHARACTERS) 

469 

470 if buff.selection_state is not None: 

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

472 # `start_selection`.) 

473 buff.selection_state.enter_shift_mode() 

474 

475 # Then move the cursor 

476 original_position = buff.cursor_position 

477 unshift_move(event) 

478 if buff.cursor_position == original_position: 

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

480 # to avoid having an empty selection 

481 buff.exit_selection() 

482 

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

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

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

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

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

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

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

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

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

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

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

494 """ 

495 Extend the selection 

496 """ 

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

498 unshift_move(event) 

499 buff = event.current_buffer 

500 

501 if buff.selection_state is not None: 

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

503 # selection is now empty, so cancel selection 

504 buff.exit_selection() 

505 

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

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

508 """ 

509 Replace selection by what is typed 

510 """ 

511 event.current_buffer.cut_selection() 

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

513 

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

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

516 """ 

517 A newline replaces the selection 

518 """ 

519 event.current_buffer.cut_selection() 

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

521 

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

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

524 """ 

525 Delete selection. 

526 """ 

527 event.current_buffer.cut_selection() 

528 

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

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

531 """ 

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

533 """ 

534 buff = event.current_buffer 

535 if buff.selection_state: 

536 buff.cut_selection() 

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

538 

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

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

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

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

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

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

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

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

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

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

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

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

551 """ 

552 Cancel selection. 

553 """ 

554 event.current_buffer.exit_selection() 

555 # we then process the cursor movement 

556 key_press = event.key_sequence[0] 

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

558 

559 return ConditionalKeyBindings(key_bindings, emacs_mode)