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