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