##// END OF EJS Templates
Switch default shortcuts for cycling auto-suggestions...
krassowski -
Show More
@@ -1,630 +1,630 b''
1 1 """
2 2 Module to define and register Terminal IPython shortcuts with
3 3 :mod:`prompt_toolkit`
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 import os
10 10 import signal
11 11 import sys
12 12 import warnings
13 13 from dataclasses import dataclass
14 14 from typing import Callable, Any, Optional, List
15 15
16 16 from prompt_toolkit.application.current import get_app
17 17 from prompt_toolkit.key_binding import KeyBindings
18 18 from prompt_toolkit.key_binding.key_processor import KeyPressEvent
19 19 from prompt_toolkit.key_binding.bindings import named_commands as nc
20 20 from prompt_toolkit.key_binding.bindings.completion import (
21 21 display_completions_like_readline,
22 22 )
23 23 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
24 24 from prompt_toolkit.filters import Condition
25 25
26 26 from IPython.core.getipython import get_ipython
27 27 from IPython.terminal.shortcuts import auto_match as match
28 28 from IPython.terminal.shortcuts import auto_suggest
29 29 from IPython.terminal.shortcuts.filters import filter_from_string
30 30 from IPython.utils.decorators import undoc
31 31
32 32 from prompt_toolkit.enums import DEFAULT_BUFFER
33 33
34 34 __all__ = ["create_ipython_shortcuts"]
35 35
36 36
37 37 @dataclass
38 38 class BaseBinding:
39 39 command: Callable[[KeyPressEvent], Any]
40 40 keys: List[str]
41 41
42 42
43 43 @dataclass
44 44 class RuntimeBinding(BaseBinding):
45 45 filter: Condition
46 46
47 47
48 48 @dataclass
49 49 class Binding(BaseBinding):
50 50 # while filter could be created by referencing variables directly (rather
51 51 # than created from strings), by using strings we ensure that users will
52 52 # be able to create filters in configuration (e.g. JSON) files too, which
53 53 # also benefits the documentation by enforcing human-readable filter names.
54 54 condition: Optional[str] = None
55 55
56 56 def __post_init__(self):
57 57 if self.condition:
58 58 self.filter = filter_from_string(self.condition)
59 59 else:
60 60 self.filter = None
61 61
62 62
63 63 def create_identifier(handler: Callable):
64 64 parts = handler.__module__.split(".")
65 65 name = handler.__name__
66 66 package = parts[0]
67 67 if len(parts) > 1:
68 68 final_module = parts[-1]
69 69 return f"{package}:{final_module}.{name}"
70 70 else:
71 71 return f"{package}:{name}"
72 72
73 73
74 74 AUTO_MATCH_BINDINGS = [
75 75 *[
76 76 Binding(
77 77 cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
78 78 )
79 79 for key, cmd in match.auto_match_parens.items()
80 80 ],
81 81 *[
82 82 # raw string
83 83 Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
84 84 for key, cmd in match.auto_match_parens_raw_string.items()
85 85 ],
86 86 Binding(
87 87 match.double_quote,
88 88 ['"'],
89 89 "focused_insert"
90 90 " & auto_match"
91 91 " & not_inside_unclosed_string"
92 92 " & preceded_by_paired_double_quotes"
93 93 " & followed_by_closing_paren_or_end",
94 94 ),
95 95 Binding(
96 96 match.single_quote,
97 97 ["'"],
98 98 "focused_insert"
99 99 " & auto_match"
100 100 " & not_inside_unclosed_string"
101 101 " & preceded_by_paired_single_quotes"
102 102 " & followed_by_closing_paren_or_end",
103 103 ),
104 104 Binding(
105 105 match.docstring_double_quotes,
106 106 ['"'],
107 107 "focused_insert"
108 108 " & auto_match"
109 109 " & not_inside_unclosed_string"
110 110 " & preceded_by_two_double_quotes",
111 111 ),
112 112 Binding(
113 113 match.docstring_single_quotes,
114 114 ["'"],
115 115 "focused_insert"
116 116 " & auto_match"
117 117 " & not_inside_unclosed_string"
118 118 " & preceded_by_two_single_quotes",
119 119 ),
120 120 Binding(
121 121 match.skip_over,
122 122 [")"],
123 123 "focused_insert & auto_match & followed_by_closing_round_paren",
124 124 ),
125 125 Binding(
126 126 match.skip_over,
127 127 ["]"],
128 128 "focused_insert & auto_match & followed_by_closing_bracket",
129 129 ),
130 130 Binding(
131 131 match.skip_over,
132 132 ["}"],
133 133 "focused_insert & auto_match & followed_by_closing_brace",
134 134 ),
135 135 Binding(
136 136 match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
137 137 ),
138 138 Binding(
139 139 match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
140 140 ),
141 141 Binding(
142 142 match.delete_pair,
143 143 ["backspace"],
144 144 "focused_insert"
145 145 " & preceded_by_opening_round_paren"
146 146 " & auto_match"
147 147 " & followed_by_closing_round_paren",
148 148 ),
149 149 Binding(
150 150 match.delete_pair,
151 151 ["backspace"],
152 152 "focused_insert"
153 153 " & preceded_by_opening_bracket"
154 154 " & auto_match"
155 155 " & followed_by_closing_bracket",
156 156 ),
157 157 Binding(
158 158 match.delete_pair,
159 159 ["backspace"],
160 160 "focused_insert"
161 161 " & preceded_by_opening_brace"
162 162 " & auto_match"
163 163 " & followed_by_closing_brace",
164 164 ),
165 165 Binding(
166 166 match.delete_pair,
167 167 ["backspace"],
168 168 "focused_insert"
169 169 " & preceded_by_double_quote"
170 170 " & auto_match"
171 171 " & followed_by_double_quote",
172 172 ),
173 173 Binding(
174 174 match.delete_pair,
175 175 ["backspace"],
176 176 "focused_insert"
177 177 " & preceded_by_single_quote"
178 178 " & auto_match"
179 179 " & followed_by_single_quote",
180 180 ),
181 181 ]
182 182
183 183 AUTO_SUGGEST_BINDINGS = [
184 184 # there are two reasons for re-defining bindings defined upstream:
185 185 # 1) prompt-toolkit does not execute autosuggestion bindings in vi mode,
186 186 # 2) prompt-toolkit checks if we are at the end of text, not end of line
187 187 # hence it does not work in multi-line mode of navigable provider
188 188 Binding(
189 189 auto_suggest.accept_or_jump_to_end,
190 190 ["end"],
191 191 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
192 192 ),
193 193 Binding(
194 194 auto_suggest.accept_or_jump_to_end,
195 195 ["c-e"],
196 196 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
197 197 ),
198 198 Binding(
199 199 auto_suggest.accept,
200 200 ["c-f"],
201 201 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
202 202 ),
203 203 Binding(
204 204 auto_suggest.accept,
205 205 ["right"],
206 206 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
207 207 ),
208 208 Binding(
209 209 auto_suggest.accept_word,
210 210 ["escape", "f"],
211 211 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
212 212 ),
213 213 Binding(
214 214 auto_suggest.accept_token,
215 215 ["c-right"],
216 216 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
217 217 ),
218 218 Binding(
219 219 auto_suggest.discard,
220 220 ["escape"],
221 221 # note this one is using `emacs_insert_mode`, not `emacs_like_insert_mode`
222 222 # as in `vi_insert_mode` we do not want `escape` to be shadowed (ever).
223 223 "has_suggestion & default_buffer_focused & emacs_insert_mode",
224 224 ),
225 225 Binding(
226 226 auto_suggest.discard,
227 227 ["delete"],
228 228 "has_suggestion & default_buffer_focused & emacs_insert_mode",
229 229 ),
230 230 Binding(
231 231 auto_suggest.swap_autosuggestion_up,
232 ["up"],
232 ["c-up"],
233 233 "navigable_suggestions"
234 234 " & ~has_line_above"
235 235 " & has_suggestion"
236 236 " & default_buffer_focused",
237 237 ),
238 238 Binding(
239 239 auto_suggest.swap_autosuggestion_down,
240 ["down"],
240 ["c-down"],
241 241 "navigable_suggestions"
242 242 " & ~has_line_below"
243 243 " & has_suggestion"
244 244 " & default_buffer_focused",
245 245 ),
246 246 Binding(
247 247 auto_suggest.up_and_update_hint,
248 ["up"],
248 ["c-up"],
249 249 "has_line_above & navigable_suggestions & default_buffer_focused",
250 250 ),
251 251 Binding(
252 252 auto_suggest.down_and_update_hint,
253 ["down"],
253 ["c-down"],
254 254 "has_line_below & navigable_suggestions & default_buffer_focused",
255 255 ),
256 256 Binding(
257 257 auto_suggest.accept_character,
258 258 ["escape", "right"],
259 259 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
260 260 ),
261 261 Binding(
262 262 auto_suggest.accept_and_move_cursor_left,
263 263 ["c-left"],
264 264 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
265 265 ),
266 266 Binding(
267 267 auto_suggest.accept_and_keep_cursor,
268 ["c-down"],
269 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
268 ["escape", "down"],
269 "has_suggestion & default_buffer_focused & emacs_insert_mode",
270 270 ),
271 271 Binding(
272 272 auto_suggest.backspace_and_resume_hint,
273 273 ["backspace"],
274 274 # no `has_suggestion` here to allow resuming if no suggestion
275 275 "default_buffer_focused & emacs_like_insert_mode",
276 276 ),
277 277 Binding(
278 278 auto_suggest.resume_hinting,
279 279 ["right"],
280 280 # For now this binding is inactive (the filter includes `never`).
281 281 # TODO: remove `never` if we reach a consensus in #13991
282 282 # TODO: use `emacs_like_insert_mode` once #13991 is in
283 283 "never & default_buffer_focused & ((vi_insert_mode & ebivim) | emacs_insert_mode)",
284 284 ),
285 285 ]
286 286
287 287
288 288 SIMPLE_CONTROL_BINDINGS = [
289 289 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
290 290 for key, cmd in {
291 291 "c-a": nc.beginning_of_line,
292 292 "c-b": nc.backward_char,
293 293 "c-k": nc.kill_line,
294 294 "c-w": nc.backward_kill_word,
295 295 "c-y": nc.yank,
296 296 "c-_": nc.undo,
297 297 }.items()
298 298 ]
299 299
300 300
301 301 ALT_AND_COMOBO_CONTROL_BINDINGS = [
302 302 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
303 303 for keys, cmd in {
304 304 # Control Combos
305 305 ("c-x", "c-e"): nc.edit_and_execute,
306 306 ("c-x", "e"): nc.edit_and_execute,
307 307 # Alt
308 308 ("escape", "b"): nc.backward_word,
309 309 ("escape", "c"): nc.capitalize_word,
310 310 ("escape", "d"): nc.kill_word,
311 311 ("escape", "h"): nc.backward_kill_word,
312 312 ("escape", "l"): nc.downcase_word,
313 313 ("escape", "u"): nc.uppercase_word,
314 314 ("escape", "y"): nc.yank_pop,
315 315 ("escape", "."): nc.yank_last_arg,
316 316 }.items()
317 317 ]
318 318
319 319
320 320 def add_binding(bindings: KeyBindings, binding: Binding):
321 321 bindings.add(
322 322 *binding.keys,
323 323 **({"filter": binding.filter} if binding.filter is not None else {}),
324 324 )(binding.command)
325 325
326 326
327 327 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
328 328 """Set up the prompt_toolkit keyboard shortcuts for IPython.
329 329
330 330 Parameters
331 331 ----------
332 332 shell: InteractiveShell
333 333 The current IPython shell Instance
334 334 skip: List[Binding]
335 335 Bindings to skip.
336 336
337 337 Returns
338 338 -------
339 339 KeyBindings
340 340 the keybinding instance for prompt toolkit.
341 341
342 342 """
343 343 kb = KeyBindings()
344 344 skip = skip or []
345 345 for binding in KEY_BINDINGS:
346 346 skip_this_one = False
347 347 for to_skip in skip:
348 348 if (
349 349 to_skip.command == binding.command
350 350 and to_skip.filter == binding.filter
351 351 and to_skip.keys == binding.keys
352 352 ):
353 353 skip_this_one = True
354 354 break
355 355 if skip_this_one:
356 356 continue
357 357 add_binding(kb, binding)
358 358
359 359 def get_input_mode(self):
360 360 app = get_app()
361 361 app.ttimeoutlen = shell.ttimeoutlen
362 362 app.timeoutlen = shell.timeoutlen
363 363
364 364 return self._input_mode
365 365
366 366 def set_input_mode(self, mode):
367 367 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
368 368 cursor = "\x1b[{} q".format(shape)
369 369
370 370 sys.stdout.write(cursor)
371 371 sys.stdout.flush()
372 372
373 373 self._input_mode = mode
374 374
375 375 if shell.editing_mode == "vi" and shell.modal_cursor:
376 376 ViState._input_mode = InputMode.INSERT # type: ignore
377 377 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
378 378
379 379 return kb
380 380
381 381
382 382 def reformat_and_execute(event):
383 383 """Reformat code and execute it"""
384 384 shell = get_ipython()
385 385 reformat_text_before_cursor(
386 386 event.current_buffer, event.current_buffer.document, shell
387 387 )
388 388 event.current_buffer.validate_and_handle()
389 389
390 390
391 391 def reformat_text_before_cursor(buffer, document, shell):
392 392 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
393 393 try:
394 394 formatted_text = shell.reformat_handler(text)
395 395 buffer.insert_text(formatted_text)
396 396 except Exception as e:
397 397 buffer.insert_text(text)
398 398
399 399
400 400 def handle_return_or_newline_or_execute(event):
401 401 shell = get_ipython()
402 402 if getattr(shell, "handle_return", None):
403 403 return shell.handle_return(shell)(event)
404 404 else:
405 405 return newline_or_execute_outer(shell)(event)
406 406
407 407
408 408 def newline_or_execute_outer(shell):
409 409 def newline_or_execute(event):
410 410 """When the user presses return, insert a newline or execute the code."""
411 411 b = event.current_buffer
412 412 d = b.document
413 413
414 414 if b.complete_state:
415 415 cc = b.complete_state.current_completion
416 416 if cc:
417 417 b.apply_completion(cc)
418 418 else:
419 419 b.cancel_completion()
420 420 return
421 421
422 422 # If there's only one line, treat it as if the cursor is at the end.
423 423 # See https://github.com/ipython/ipython/issues/10425
424 424 if d.line_count == 1:
425 425 check_text = d.text
426 426 else:
427 427 check_text = d.text[: d.cursor_position]
428 428 status, indent = shell.check_complete(check_text)
429 429
430 430 # if all we have after the cursor is whitespace: reformat current text
431 431 # before cursor
432 432 after_cursor = d.text[d.cursor_position :]
433 433 reformatted = False
434 434 if not after_cursor.strip():
435 435 reformat_text_before_cursor(b, d, shell)
436 436 reformatted = True
437 437 if not (
438 438 d.on_last_line
439 439 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
440 440 ):
441 441 if shell.autoindent:
442 442 b.insert_text("\n" + indent)
443 443 else:
444 444 b.insert_text("\n")
445 445 return
446 446
447 447 if (status != "incomplete") and b.accept_handler:
448 448 if not reformatted:
449 449 reformat_text_before_cursor(b, d, shell)
450 450 b.validate_and_handle()
451 451 else:
452 452 if shell.autoindent:
453 453 b.insert_text("\n" + indent)
454 454 else:
455 455 b.insert_text("\n")
456 456
457 457 return newline_or_execute
458 458
459 459
460 460 def previous_history_or_previous_completion(event):
461 461 """
462 462 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
463 463
464 464 If completer is open this still select previous completion.
465 465 """
466 466 event.current_buffer.auto_up()
467 467
468 468
469 469 def next_history_or_next_completion(event):
470 470 """
471 471 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
472 472
473 473 If completer is open this still select next completion.
474 474 """
475 475 event.current_buffer.auto_down()
476 476
477 477
478 478 def dismiss_completion(event):
479 479 """Dismiss completion"""
480 480 b = event.current_buffer
481 481 if b.complete_state:
482 482 b.cancel_completion()
483 483
484 484
485 485 def reset_buffer(event):
486 486 """Reset buffer"""
487 487 b = event.current_buffer
488 488 if b.complete_state:
489 489 b.cancel_completion()
490 490 else:
491 491 b.reset()
492 492
493 493
494 494 def reset_search_buffer(event):
495 495 """Reset search buffer"""
496 496 if event.current_buffer.document.text:
497 497 event.current_buffer.reset()
498 498 else:
499 499 event.app.layout.focus(DEFAULT_BUFFER)
500 500
501 501
502 502 def suspend_to_bg(event):
503 503 """Suspend to background"""
504 504 event.app.suspend_to_background()
505 505
506 506
507 507 def quit(event):
508 508 """
509 509 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
510 510
511 511 On platforms that support SIGQUIT, send SIGQUIT to the current process.
512 512 On other platforms, just exit the process with a message.
513 513 """
514 514 sigquit = getattr(signal, "SIGQUIT", None)
515 515 if sigquit is not None:
516 516 os.kill(0, signal.SIGQUIT)
517 517 else:
518 518 sys.exit("Quit")
519 519
520 520
521 521 def indent_buffer(event):
522 522 """Indent buffer"""
523 523 event.current_buffer.insert_text(" " * 4)
524 524
525 525
526 526 def newline_autoindent(event):
527 527 """Insert a newline after the cursor indented appropriately.
528 528
529 529 Fancier version of former ``newline_with_copy_margin`` which should
530 530 compute the correct indentation of the inserted line. That is to say, indent
531 531 by 4 extra space after a function definition, class definition, context
532 532 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
533 533 """
534 534 shell = get_ipython()
535 535 inputsplitter = shell.input_transformer_manager
536 536 b = event.current_buffer
537 537 d = b.document
538 538
539 539 if b.complete_state:
540 540 b.cancel_completion()
541 541 text = d.text[: d.cursor_position] + "\n"
542 542 _, indent = inputsplitter.check_complete(text)
543 543 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
544 544
545 545
546 546 def open_input_in_editor(event):
547 547 """Open code from input in external editor"""
548 548 event.app.current_buffer.open_in_editor()
549 549
550 550
551 551 if sys.platform == "win32":
552 552 from IPython.core.error import TryNext
553 553 from IPython.lib.clipboard import (
554 554 ClipboardEmpty,
555 555 tkinter_clipboard_get,
556 556 win32_clipboard_get,
557 557 )
558 558
559 559 @undoc
560 560 def win_paste(event):
561 561 try:
562 562 text = win32_clipboard_get()
563 563 except TryNext:
564 564 try:
565 565 text = tkinter_clipboard_get()
566 566 except (TryNext, ClipboardEmpty):
567 567 return
568 568 except ClipboardEmpty:
569 569 return
570 570 event.current_buffer.insert_text(text.replace("\t", " " * 4))
571 571
572 572 else:
573 573
574 574 @undoc
575 575 def win_paste(event):
576 576 """Stub used on other platforms"""
577 577 pass
578 578
579 579
580 580 KEY_BINDINGS = [
581 581 Binding(
582 582 handle_return_or_newline_or_execute,
583 583 ["enter"],
584 584 "default_buffer_focused & ~has_selection & insert_mode",
585 585 ),
586 586 Binding(
587 587 reformat_and_execute,
588 588 ["escape", "enter"],
589 589 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
590 590 ),
591 591 Binding(quit, ["c-\\"]),
592 592 Binding(
593 593 previous_history_or_previous_completion,
594 594 ["c-p"],
595 595 "vi_insert_mode & default_buffer_focused",
596 596 ),
597 597 Binding(
598 598 next_history_or_next_completion,
599 599 ["c-n"],
600 600 "vi_insert_mode & default_buffer_focused",
601 601 ),
602 602 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
603 603 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
604 604 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
605 605 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
606 606 Binding(
607 607 indent_buffer,
608 608 ["tab"], # Ctrl+I == Tab
609 609 "default_buffer_focused"
610 610 " & ~has_selection"
611 611 " & insert_mode"
612 612 " & cursor_in_leading_ws",
613 613 ),
614 614 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
615 615 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
616 616 *AUTO_MATCH_BINDINGS,
617 617 *AUTO_SUGGEST_BINDINGS,
618 618 Binding(
619 619 display_completions_like_readline,
620 620 ["c-i"],
621 621 "readline_like_completions"
622 622 " & default_buffer_focused"
623 623 " & ~has_selection"
624 624 " & insert_mode"
625 625 " & ~cursor_in_leading_ws",
626 626 ),
627 627 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
628 628 *SIMPLE_CONTROL_BINDINGS,
629 629 *ALT_AND_COMOBO_CONTROL_BINDINGS,
630 630 ]
General Comments 0
You need to be logged in to leave comments. Login now