##// END OF EJS Templates
Backport PR #14027 on branch 8.12.x (Resume hinting on right arrow press by default) (#14042)...
Matthias Bussonnier -
r28253:887da037 merge
parent child Browse files
Show More
@@ -1,630 +1,627 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 232 ["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 240 ["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 248 ["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 253 ["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 268 ["c-down"],
269 269 "has_suggestion & default_buffer_focused & emacs_like_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 # For now this binding is inactive (the filter includes `never`).
281 # TODO: remove `never` if we reach a consensus in #13991
282 # TODO: use `emacs_like_insert_mode` once #13991 is in
283 "never & default_buffer_focused & ((vi_insert_mode & ebivim) | emacs_insert_mode)",
280 "default_buffer_focused & emacs_like_insert_mode",
284 281 ),
285 282 ]
286 283
287 284
288 285 SIMPLE_CONTROL_BINDINGS = [
289 286 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
290 287 for key, cmd in {
291 288 "c-a": nc.beginning_of_line,
292 289 "c-b": nc.backward_char,
293 290 "c-k": nc.kill_line,
294 291 "c-w": nc.backward_kill_word,
295 292 "c-y": nc.yank,
296 293 "c-_": nc.undo,
297 294 }.items()
298 295 ]
299 296
300 297
301 298 ALT_AND_COMOBO_CONTROL_BINDINGS = [
302 299 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
303 300 for keys, cmd in {
304 301 # Control Combos
305 302 ("c-x", "c-e"): nc.edit_and_execute,
306 303 ("c-x", "e"): nc.edit_and_execute,
307 304 # Alt
308 305 ("escape", "b"): nc.backward_word,
309 306 ("escape", "c"): nc.capitalize_word,
310 307 ("escape", "d"): nc.kill_word,
311 308 ("escape", "h"): nc.backward_kill_word,
312 309 ("escape", "l"): nc.downcase_word,
313 310 ("escape", "u"): nc.uppercase_word,
314 311 ("escape", "y"): nc.yank_pop,
315 312 ("escape", "."): nc.yank_last_arg,
316 313 }.items()
317 314 ]
318 315
319 316
320 317 def add_binding(bindings: KeyBindings, binding: Binding):
321 318 bindings.add(
322 319 *binding.keys,
323 320 **({"filter": binding.filter} if binding.filter is not None else {}),
324 321 )(binding.command)
325 322
326 323
327 324 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
328 325 """Set up the prompt_toolkit keyboard shortcuts for IPython.
329 326
330 327 Parameters
331 328 ----------
332 329 shell: InteractiveShell
333 330 The current IPython shell Instance
334 331 skip: List[Binding]
335 332 Bindings to skip.
336 333
337 334 Returns
338 335 -------
339 336 KeyBindings
340 337 the keybinding instance for prompt toolkit.
341 338
342 339 """
343 340 kb = KeyBindings()
344 341 skip = skip or []
345 342 for binding in KEY_BINDINGS:
346 343 skip_this_one = False
347 344 for to_skip in skip:
348 345 if (
349 346 to_skip.command == binding.command
350 347 and to_skip.filter == binding.filter
351 348 and to_skip.keys == binding.keys
352 349 ):
353 350 skip_this_one = True
354 351 break
355 352 if skip_this_one:
356 353 continue
357 354 add_binding(kb, binding)
358 355
359 356 def get_input_mode(self):
360 357 app = get_app()
361 358 app.ttimeoutlen = shell.ttimeoutlen
362 359 app.timeoutlen = shell.timeoutlen
363 360
364 361 return self._input_mode
365 362
366 363 def set_input_mode(self, mode):
367 364 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
368 365 cursor = "\x1b[{} q".format(shape)
369 366
370 367 sys.stdout.write(cursor)
371 368 sys.stdout.flush()
372 369
373 370 self._input_mode = mode
374 371
375 372 if shell.editing_mode == "vi" and shell.modal_cursor:
376 373 ViState._input_mode = InputMode.INSERT # type: ignore
377 374 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
378 375
379 376 return kb
380 377
381 378
382 379 def reformat_and_execute(event):
383 380 """Reformat code and execute it"""
384 381 shell = get_ipython()
385 382 reformat_text_before_cursor(
386 383 event.current_buffer, event.current_buffer.document, shell
387 384 )
388 385 event.current_buffer.validate_and_handle()
389 386
390 387
391 388 def reformat_text_before_cursor(buffer, document, shell):
392 389 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
393 390 try:
394 391 formatted_text = shell.reformat_handler(text)
395 392 buffer.insert_text(formatted_text)
396 393 except Exception as e:
397 394 buffer.insert_text(text)
398 395
399 396
400 397 def handle_return_or_newline_or_execute(event):
401 398 shell = get_ipython()
402 399 if getattr(shell, "handle_return", None):
403 400 return shell.handle_return(shell)(event)
404 401 else:
405 402 return newline_or_execute_outer(shell)(event)
406 403
407 404
408 405 def newline_or_execute_outer(shell):
409 406 def newline_or_execute(event):
410 407 """When the user presses return, insert a newline or execute the code."""
411 408 b = event.current_buffer
412 409 d = b.document
413 410
414 411 if b.complete_state:
415 412 cc = b.complete_state.current_completion
416 413 if cc:
417 414 b.apply_completion(cc)
418 415 else:
419 416 b.cancel_completion()
420 417 return
421 418
422 419 # If there's only one line, treat it as if the cursor is at the end.
423 420 # See https://github.com/ipython/ipython/issues/10425
424 421 if d.line_count == 1:
425 422 check_text = d.text
426 423 else:
427 424 check_text = d.text[: d.cursor_position]
428 425 status, indent = shell.check_complete(check_text)
429 426
430 427 # if all we have after the cursor is whitespace: reformat current text
431 428 # before cursor
432 429 after_cursor = d.text[d.cursor_position :]
433 430 reformatted = False
434 431 if not after_cursor.strip():
435 432 reformat_text_before_cursor(b, d, shell)
436 433 reformatted = True
437 434 if not (
438 435 d.on_last_line
439 436 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
440 437 ):
441 438 if shell.autoindent:
442 439 b.insert_text("\n" + indent)
443 440 else:
444 441 b.insert_text("\n")
445 442 return
446 443
447 444 if (status != "incomplete") and b.accept_handler:
448 445 if not reformatted:
449 446 reformat_text_before_cursor(b, d, shell)
450 447 b.validate_and_handle()
451 448 else:
452 449 if shell.autoindent:
453 450 b.insert_text("\n" + indent)
454 451 else:
455 452 b.insert_text("\n")
456 453
457 454 return newline_or_execute
458 455
459 456
460 457 def previous_history_or_previous_completion(event):
461 458 """
462 459 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
463 460
464 461 If completer is open this still select previous completion.
465 462 """
466 463 event.current_buffer.auto_up()
467 464
468 465
469 466 def next_history_or_next_completion(event):
470 467 """
471 468 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
472 469
473 470 If completer is open this still select next completion.
474 471 """
475 472 event.current_buffer.auto_down()
476 473
477 474
478 475 def dismiss_completion(event):
479 476 """Dismiss completion"""
480 477 b = event.current_buffer
481 478 if b.complete_state:
482 479 b.cancel_completion()
483 480
484 481
485 482 def reset_buffer(event):
486 483 """Reset buffer"""
487 484 b = event.current_buffer
488 485 if b.complete_state:
489 486 b.cancel_completion()
490 487 else:
491 488 b.reset()
492 489
493 490
494 491 def reset_search_buffer(event):
495 492 """Reset search buffer"""
496 493 if event.current_buffer.document.text:
497 494 event.current_buffer.reset()
498 495 else:
499 496 event.app.layout.focus(DEFAULT_BUFFER)
500 497
501 498
502 499 def suspend_to_bg(event):
503 500 """Suspend to background"""
504 501 event.app.suspend_to_background()
505 502
506 503
507 504 def quit(event):
508 505 """
509 506 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
510 507
511 508 On platforms that support SIGQUIT, send SIGQUIT to the current process.
512 509 On other platforms, just exit the process with a message.
513 510 """
514 511 sigquit = getattr(signal, "SIGQUIT", None)
515 512 if sigquit is not None:
516 513 os.kill(0, signal.SIGQUIT)
517 514 else:
518 515 sys.exit("Quit")
519 516
520 517
521 518 def indent_buffer(event):
522 519 """Indent buffer"""
523 520 event.current_buffer.insert_text(" " * 4)
524 521
525 522
526 523 def newline_autoindent(event):
527 524 """Insert a newline after the cursor indented appropriately.
528 525
529 526 Fancier version of former ``newline_with_copy_margin`` which should
530 527 compute the correct indentation of the inserted line. That is to say, indent
531 528 by 4 extra space after a function definition, class definition, context
532 529 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
533 530 """
534 531 shell = get_ipython()
535 532 inputsplitter = shell.input_transformer_manager
536 533 b = event.current_buffer
537 534 d = b.document
538 535
539 536 if b.complete_state:
540 537 b.cancel_completion()
541 538 text = d.text[: d.cursor_position] + "\n"
542 539 _, indent = inputsplitter.check_complete(text)
543 540 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
544 541
545 542
546 543 def open_input_in_editor(event):
547 544 """Open code from input in external editor"""
548 545 event.app.current_buffer.open_in_editor()
549 546
550 547
551 548 if sys.platform == "win32":
552 549 from IPython.core.error import TryNext
553 550 from IPython.lib.clipboard import (
554 551 ClipboardEmpty,
555 552 tkinter_clipboard_get,
556 553 win32_clipboard_get,
557 554 )
558 555
559 556 @undoc
560 557 def win_paste(event):
561 558 try:
562 559 text = win32_clipboard_get()
563 560 except TryNext:
564 561 try:
565 562 text = tkinter_clipboard_get()
566 563 except (TryNext, ClipboardEmpty):
567 564 return
568 565 except ClipboardEmpty:
569 566 return
570 567 event.current_buffer.insert_text(text.replace("\t", " " * 4))
571 568
572 569 else:
573 570
574 571 @undoc
575 572 def win_paste(event):
576 573 """Stub used on other platforms"""
577 574 pass
578 575
579 576
580 577 KEY_BINDINGS = [
581 578 Binding(
582 579 handle_return_or_newline_or_execute,
583 580 ["enter"],
584 581 "default_buffer_focused & ~has_selection & insert_mode",
585 582 ),
586 583 Binding(
587 584 reformat_and_execute,
588 585 ["escape", "enter"],
589 586 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
590 587 ),
591 588 Binding(quit, ["c-\\"]),
592 589 Binding(
593 590 previous_history_or_previous_completion,
594 591 ["c-p"],
595 592 "vi_insert_mode & default_buffer_focused",
596 593 ),
597 594 Binding(
598 595 next_history_or_next_completion,
599 596 ["c-n"],
600 597 "vi_insert_mode & default_buffer_focused",
601 598 ),
602 599 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
603 600 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
604 601 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
605 602 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
606 603 Binding(
607 604 indent_buffer,
608 605 ["tab"], # Ctrl+I == Tab
609 606 "default_buffer_focused"
610 607 " & ~has_selection"
611 608 " & insert_mode"
612 609 " & cursor_in_leading_ws",
613 610 ),
614 611 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
615 612 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
616 613 *AUTO_MATCH_BINDINGS,
617 614 *AUTO_SUGGEST_BINDINGS,
618 615 Binding(
619 616 display_completions_like_readline,
620 617 ["c-i"],
621 618 "readline_like_completions"
622 619 " & default_buffer_focused"
623 620 " & ~has_selection"
624 621 " & insert_mode"
625 622 " & ~cursor_in_leading_ws",
626 623 ),
627 624 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
628 625 *SIMPLE_CONTROL_BINDINGS,
629 626 *ALT_AND_COMOBO_CONTROL_BINDINGS,
630 627 ]
General Comments 0
You need to be logged in to leave comments. Login now