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