##// END OF EJS Templates
PR: Unassigned allowed commands (#14564)...
M Bussonnier -
r28945:08be4c99 merge
parent child Browse files
Show More
@@ -1,1021 +1,1030
1 1 """IPython terminal interface using prompt_toolkit"""
2 2
3 3 import os
4 4 import sys
5 5 import inspect
6 6 from warnings import warn
7 7 from typing import Union as UnionType, Optional
8 8
9 9 from IPython.core.async_helpers import get_asyncio_loop
10 10 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
11 11 from IPython.utils.py3compat import input
12 12 from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title
13 13 from IPython.utils.process import abbrev_cwd
14 14 from traitlets import (
15 15 Bool,
16 16 Unicode,
17 17 Dict,
18 18 Integer,
19 19 List,
20 20 observe,
21 21 Instance,
22 22 Type,
23 23 default,
24 24 Enum,
25 25 Union,
26 26 Any,
27 27 validate,
28 28 Float,
29 29 )
30 30
31 31 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
32 32 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
33 33 from prompt_toolkit.filters import HasFocus, Condition, IsDone
34 34 from prompt_toolkit.formatted_text import PygmentsTokens
35 35 from prompt_toolkit.history import History
36 36 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
37 37 from prompt_toolkit.output import ColorDepth
38 38 from prompt_toolkit.patch_stdout import patch_stdout
39 39 from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
40 40 from prompt_toolkit.styles import DynamicStyle, merge_styles
41 41 from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
42 42 from prompt_toolkit import __version__ as ptk_version
43 43
44 44 from pygments.styles import get_style_by_name
45 45 from pygments.style import Style
46 46 from pygments.token import Token
47 47
48 48 from .debugger import TerminalPdb, Pdb
49 49 from .magics import TerminalMagics
50 50 from .pt_inputhooks import get_inputhook_name_and_func
51 51 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
52 52 from .ptutils import IPythonPTCompleter, IPythonPTLexer
53 53 from .shortcuts import (
54 54 KEY_BINDINGS,
55 UNASSIGNED_ALLOWED_COMMANDS,
55 56 create_ipython_shortcuts,
56 57 create_identifier,
57 58 RuntimeBinding,
58 59 add_binding,
59 60 )
60 61 from .shortcuts.filters import KEYBINDING_FILTERS, filter_from_string
61 62 from .shortcuts.auto_suggest import (
62 63 NavigableAutoSuggestFromHistory,
63 64 AppendAutoSuggestionInAnyLine,
64 65 )
65 66
66 67 PTK3 = ptk_version.startswith('3.')
67 68
68 69
69 70 class _NoStyle(Style):
70 71 pass
71 72
72 73
73 74 _style_overrides_light_bg = {
74 75 Token.Prompt: '#ansibrightblue',
75 76 Token.PromptNum: '#ansiblue bold',
76 77 Token.OutPrompt: '#ansibrightred',
77 78 Token.OutPromptNum: '#ansired bold',
78 79 }
79 80
80 81 _style_overrides_linux = {
81 82 Token.Prompt: '#ansibrightgreen',
82 83 Token.PromptNum: '#ansigreen bold',
83 84 Token.OutPrompt: '#ansibrightred',
84 85 Token.OutPromptNum: '#ansired bold',
85 86 }
86 87
87 88
88 89 def _backward_compat_continuation_prompt_tokens(method, width: int, *, lineno: int):
89 90 """
90 91 Sagemath use custom prompt and we broke them in 8.19.
91 92 """
92 93 sig = inspect.signature(method)
93 94 if "lineno" in inspect.signature(method).parameters or any(
94 95 [p.kind == p.VAR_KEYWORD for p in sig.parameters.values()]
95 96 ):
96 97 return method(width, lineno=lineno)
97 98 else:
98 99 return method(width)
99 100
100 101
101 102 def get_default_editor():
102 103 try:
103 104 return os.environ['EDITOR']
104 105 except KeyError:
105 106 pass
106 107 except UnicodeError:
107 108 warn("$EDITOR environment variable is not pure ASCII. Using platform "
108 109 "default editor.")
109 110
110 111 if os.name == 'posix':
111 112 return 'vi' # the only one guaranteed to be there!
112 113 else:
113 114 return "notepad" # same in Windows!
114 115
115 116
116 117 # conservatively check for tty
117 118 # overridden streams can result in things like:
118 119 # - sys.stdin = None
119 120 # - no isatty method
120 121 for _name in ('stdin', 'stdout', 'stderr'):
121 122 _stream = getattr(sys, _name)
122 123 try:
123 124 if not _stream or not hasattr(_stream, "isatty") or not _stream.isatty():
124 125 _is_tty = False
125 126 break
126 127 except ValueError:
127 128 # stream is closed
128 129 _is_tty = False
129 130 break
130 131 else:
131 132 _is_tty = True
132 133
133 134
134 135 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
135 136
136 137 def black_reformat_handler(text_before_cursor):
137 138 """
138 139 We do not need to protect against error,
139 140 this is taken care at a higher level where any reformat error is ignored.
140 141 Indeed we may call reformatting on incomplete code.
141 142 """
142 143 import black
143 144
144 145 formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
145 146 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
146 147 formatted_text = formatted_text[:-1]
147 148 return formatted_text
148 149
149 150
150 151 def yapf_reformat_handler(text_before_cursor):
151 152 from yapf.yapflib import file_resources
152 153 from yapf.yapflib import yapf_api
153 154
154 155 style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
155 156 formatted_text, was_formatted = yapf_api.FormatCode(
156 157 text_before_cursor, style_config=style_config
157 158 )
158 159 if was_formatted:
159 160 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
160 161 formatted_text = formatted_text[:-1]
161 162 return formatted_text
162 163 else:
163 164 return text_before_cursor
164 165
165 166
166 167 class PtkHistoryAdapter(History):
167 168 """
168 169 Prompt toolkit has it's own way of handling history, Where it assumes it can
169 170 Push/pull from history.
170 171
171 172 """
172 173
173 174 def __init__(self, shell):
174 175 super().__init__()
175 176 self.shell = shell
176 177 self._refresh()
177 178
178 179 def append_string(self, string):
179 180 # we rely on sql for that.
180 181 self._loaded = False
181 182 self._refresh()
182 183
183 184 def _refresh(self):
184 185 if not self._loaded:
185 186 self._loaded_strings = list(self.load_history_strings())
186 187
187 188 def load_history_strings(self):
188 189 last_cell = ""
189 190 res = []
190 191 for __, ___, cell in self.shell.history_manager.get_tail(
191 192 self.shell.history_load_length, include_latest=True
192 193 ):
193 194 # Ignore blank lines and consecutive duplicates
194 195 cell = cell.rstrip()
195 196 if cell and (cell != last_cell):
196 197 res.append(cell)
197 198 last_cell = cell
198 199 yield from res[::-1]
199 200
200 201 def store_string(self, string: str) -> None:
201 202 pass
202 203
203 204 class TerminalInteractiveShell(InteractiveShell):
204 205 mime_renderers = Dict().tag(config=True)
205 206
206 207 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
207 208 'to reserve for the tab completion menu, '
208 209 'search history, ...etc, the height of '
209 210 'these menus will at most this value. '
210 211 'Increase it is you prefer long and skinny '
211 212 'menus, decrease for short and wide.'
212 213 ).tag(config=True)
213 214
214 215 pt_app: UnionType[PromptSession, None] = None
215 216 auto_suggest: UnionType[
216 217 AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None
217 218 ] = None
218 219 debugger_history = None
219 220
220 221 debugger_history_file = Unicode(
221 222 "~/.pdbhistory", help="File in which to store and read history"
222 223 ).tag(config=True)
223 224
224 225 simple_prompt = Bool(_use_simple_prompt,
225 226 help="""Use `raw_input` for the REPL, without completion and prompt colors.
226 227
227 228 Useful when controlling IPython as a subprocess, and piping
228 229 STDIN/OUT/ERR. Known usage are: IPython's own testing machinery,
229 230 and emacs' inferior-python subprocess (assuming you have set
230 231 `python-shell-interpreter` to "ipython") available through the
231 232 built-in `M-x run-python` and third party packages such as elpy.
232 233
233 234 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
234 235 environment variable is set, or the current terminal is not a tty.
235 236 Thus the Default value reported in --help-all, or config will often
236 237 be incorrectly reported.
237 238 """,
238 239 ).tag(config=True)
239 240
240 241 @property
241 242 def debugger_cls(self):
242 243 return Pdb if self.simple_prompt else TerminalPdb
243 244
244 245 confirm_exit = Bool(True,
245 246 help="""
246 247 Set to confirm when you try to exit IPython with an EOF (Control-D
247 248 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
248 249 you can force a direct exit without any confirmation.""",
249 250 ).tag(config=True)
250 251
251 252 editing_mode = Unicode('emacs',
252 253 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
253 254 ).tag(config=True)
254 255
255 256 emacs_bindings_in_vi_insert_mode = Bool(
256 257 True,
257 258 help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.",
258 259 ).tag(config=True)
259 260
260 261 modal_cursor = Bool(
261 262 True,
262 263 help="""
263 264 Cursor shape changes depending on vi mode: beam in vi insert mode,
264 265 block in nav mode, underscore in replace mode.""",
265 266 ).tag(config=True)
266 267
267 268 ttimeoutlen = Float(
268 269 0.01,
269 270 help="""The time in milliseconds that is waited for a key code
270 271 to complete.""",
271 272 ).tag(config=True)
272 273
273 274 timeoutlen = Float(
274 275 0.5,
275 276 help="""The time in milliseconds that is waited for a mapped key
276 277 sequence to complete.""",
277 278 ).tag(config=True)
278 279
279 280 autoformatter = Unicode(
280 281 None,
281 282 help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`",
282 283 allow_none=True
283 284 ).tag(config=True)
284 285
285 286 auto_match = Bool(
286 287 False,
287 288 help="""
288 289 Automatically add/delete closing bracket or quote when opening bracket or quote is entered/deleted.
289 290 Brackets: (), [], {}
290 291 Quotes: '', \"\"
291 292 """,
292 293 ).tag(config=True)
293 294
294 295 mouse_support = Bool(False,
295 296 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
296 297 ).tag(config=True)
297 298
298 299 # We don't load the list of styles for the help string, because loading
299 300 # Pygments plugins takes time and can cause unexpected errors.
300 301 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
301 302 help="""The name or class of a Pygments style to use for syntax
302 303 highlighting. To see available styles, run `pygmentize -L styles`."""
303 304 ).tag(config=True)
304 305
305 306 @validate('editing_mode')
306 307 def _validate_editing_mode(self, proposal):
307 308 if proposal['value'].lower() == 'vim':
308 309 proposal['value']= 'vi'
309 310 elif proposal['value'].lower() == 'default':
310 311 proposal['value']= 'emacs'
311 312
312 313 if hasattr(EditingMode, proposal['value'].upper()):
313 314 return proposal['value'].lower()
314 315
315 316 return self.editing_mode
316 317
317 318 @observe('editing_mode')
318 319 def _editing_mode(self, change):
319 320 if self.pt_app:
320 321 self.pt_app.editing_mode = getattr(EditingMode, change.new.upper())
321 322
322 323 def _set_formatter(self, formatter):
323 324 if formatter is None:
324 325 self.reformat_handler = lambda x:x
325 326 elif formatter == 'black':
326 327 self.reformat_handler = black_reformat_handler
327 328 elif formatter == "yapf":
328 329 self.reformat_handler = yapf_reformat_handler
329 330 else:
330 331 raise ValueError
331 332
332 333 @observe("autoformatter")
333 334 def _autoformatter_changed(self, change):
334 335 formatter = change.new
335 336 self._set_formatter(formatter)
336 337
337 338 @observe('highlighting_style')
338 339 @observe('colors')
339 340 def _highlighting_style_changed(self, change):
340 341 self.refresh_style()
341 342
342 343 def refresh_style(self):
343 344 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
344 345
345 346 highlighting_style_overrides = Dict(
346 347 help="Override highlighting format for specific tokens"
347 348 ).tag(config=True)
348 349
349 350 true_color = Bool(False,
350 351 help="""Use 24bit colors instead of 256 colors in prompt highlighting.
351 352 If your terminal supports true color, the following command should
352 353 print ``TRUECOLOR`` in orange::
353 354
354 355 printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"
355 356 """,
356 357 ).tag(config=True)
357 358
358 359 editor = Unicode(get_default_editor(),
359 360 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
360 361 ).tag(config=True)
361 362
362 363 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
363 364
364 365 prompts = Instance(Prompts)
365 366
366 367 @default('prompts')
367 368 def _prompts_default(self):
368 369 return self.prompts_class(self)
369 370
370 371 # @observe('prompts')
371 372 # def _(self, change):
372 373 # self._update_layout()
373 374
374 375 @default('displayhook_class')
375 376 def _displayhook_class_default(self):
376 377 return RichPromptDisplayHook
377 378
378 379 term_title = Bool(True,
379 380 help="Automatically set the terminal title"
380 381 ).tag(config=True)
381 382
382 383 term_title_format = Unicode("IPython: {cwd}",
383 384 help="Customize the terminal title format. This is a python format string. " +
384 385 "Available substitutions are: {cwd}."
385 386 ).tag(config=True)
386 387
387 388 display_completions = Enum(('column', 'multicolumn','readlinelike'),
388 389 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
389 390 "'readlinelike'. These options are for `prompt_toolkit`, see "
390 391 "`prompt_toolkit` documentation for more information."
391 392 ),
392 393 default_value='multicolumn').tag(config=True)
393 394
394 395 highlight_matching_brackets = Bool(True,
395 396 help="Highlight matching brackets.",
396 397 ).tag(config=True)
397 398
398 399 extra_open_editor_shortcuts = Bool(False,
399 400 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
400 401 "This is in addition to the F2 binding, which is always enabled."
401 402 ).tag(config=True)
402 403
403 404 handle_return = Any(None,
404 405 help="Provide an alternative handler to be called when the user presses "
405 406 "Return. This is an advanced option intended for debugging, which "
406 407 "may be changed or removed in later releases."
407 408 ).tag(config=True)
408 409
409 410 enable_history_search = Bool(True,
410 411 help="Allows to enable/disable the prompt toolkit history search"
411 412 ).tag(config=True)
412 413
413 414 autosuggestions_provider = Unicode(
414 415 "NavigableAutoSuggestFromHistory",
415 416 help="Specifies from which source automatic suggestions are provided. "
416 417 "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and "
417 418 ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, "
418 419 " or ``None`` to disable automatic suggestions. "
419 420 "Default is `'NavigableAutoSuggestFromHistory`'.",
420 421 allow_none=True,
421 422 ).tag(config=True)
422 423
423 424 def _set_autosuggestions(self, provider):
424 425 # disconnect old handler
425 426 if self.auto_suggest and isinstance(
426 427 self.auto_suggest, NavigableAutoSuggestFromHistory
427 428 ):
428 429 self.auto_suggest.disconnect()
429 430 if provider is None:
430 431 self.auto_suggest = None
431 432 elif provider == "AutoSuggestFromHistory":
432 433 self.auto_suggest = AutoSuggestFromHistory()
433 434 elif provider == "NavigableAutoSuggestFromHistory":
434 435 self.auto_suggest = NavigableAutoSuggestFromHistory()
435 436 else:
436 437 raise ValueError("No valid provider.")
437 438 if self.pt_app:
438 439 self.pt_app.auto_suggest = self.auto_suggest
439 440
440 441 @observe("autosuggestions_provider")
441 442 def _autosuggestions_provider_changed(self, change):
442 443 provider = change.new
443 444 self._set_autosuggestions(provider)
444 445
445 446 shortcuts = List(
446 447 trait=Dict(
447 448 key_trait=Enum(
448 449 [
449 450 "command",
450 451 "match_keys",
451 452 "match_filter",
452 453 "new_keys",
453 454 "new_filter",
454 455 "create",
455 456 ]
456 457 ),
457 458 per_key_traits={
458 459 "command": Unicode(),
459 460 "match_keys": List(Unicode()),
460 461 "match_filter": Unicode(),
461 462 "new_keys": List(Unicode()),
462 463 "new_filter": Unicode(),
463 464 "create": Bool(False),
464 465 },
465 466 ),
466 467 help="""Add, disable or modifying shortcuts.
467 468
468 469 Each entry on the list should be a dictionary with ``command`` key
469 470 identifying the target function executed by the shortcut and at least
470 471 one of the following:
471 472
472 473 - ``match_keys``: list of keys used to match an existing shortcut,
473 474 - ``match_filter``: shortcut filter used to match an existing shortcut,
474 475 - ``new_keys``: list of keys to set,
475 476 - ``new_filter``: a new shortcut filter to set
476 477
477 478 The filters have to be composed of pre-defined verbs and joined by one
478 479 of the following conjunctions: ``&`` (and), ``|`` (or), ``~`` (not).
479 480 The pre-defined verbs are:
480 481
481 482 {}
482 483
483 484
484 485 To disable a shortcut set ``new_keys`` to an empty list.
485 486 To add a shortcut add key ``create`` with value ``True``.
486 487
487 488 When modifying/disabling shortcuts, ``match_keys``/``match_filter`` can
488 489 be omitted if the provided specification uniquely identifies a shortcut
489 490 to be modified/disabled. When modifying a shortcut ``new_filter`` or
490 491 ``new_keys`` can be omitted which will result in reuse of the existing
491 492 filter/keys.
492 493
493 494 Only shortcuts defined in IPython (and not default prompt-toolkit
494 495 shortcuts) can be modified or disabled. The full list of shortcuts,
495 496 command identifiers and filters is available under
496 497 :ref:`terminal-shortcuts-list`.
497 498 """.format(
498 499 "\n ".join([f"- `{k}`" for k in KEYBINDING_FILTERS])
499 500 ),
500 501 ).tag(config=True)
501 502
502 503 @observe("shortcuts")
503 504 def _shortcuts_changed(self, change):
504 505 if self.pt_app:
505 506 self.pt_app.key_bindings = self._merge_shortcuts(user_shortcuts=change.new)
506 507
507 508 def _merge_shortcuts(self, user_shortcuts):
508 509 # rebuild the bindings list from scratch
509 510 key_bindings = create_ipython_shortcuts(self)
510 511
511 # for now we only allow adding shortcuts for commands which are already
512 # registered; this is a security precaution.
513 known_commands = {
512 # for now we only allow adding shortcuts for a specific set of
513 # commands; this is a security precution.
514 allowed_commands = {
514 515 create_identifier(binding.command): binding.command
515 516 for binding in KEY_BINDINGS
516 517 }
518 allowed_commands.update(
519 {
520 create_identifier(command): command
521 for command in UNASSIGNED_ALLOWED_COMMANDS
522 }
523 )
517 524 shortcuts_to_skip = []
518 525 shortcuts_to_add = []
519 526
520 527 for shortcut in user_shortcuts:
521 528 command_id = shortcut["command"]
522 if command_id not in known_commands:
523 allowed_commands = "\n - ".join(known_commands)
529 if command_id not in allowed_commands:
530 allowed_commands = "\n - ".join(allowed_commands)
524 531 raise ValueError(
525 532 f"{command_id} is not a known shortcut command."
526 533 f" Allowed commands are: \n - {allowed_commands}"
527 534 )
528 535 old_keys = shortcut.get("match_keys", None)
529 536 old_filter = (
530 537 filter_from_string(shortcut["match_filter"])
531 538 if "match_filter" in shortcut
532 539 else None
533 540 )
534 541 matching = [
535 542 binding
536 543 for binding in KEY_BINDINGS
537 544 if (
538 545 (old_filter is None or binding.filter == old_filter)
539 546 and (old_keys is None or [k for k in binding.keys] == old_keys)
540 547 and create_identifier(binding.command) == command_id
541 548 )
542 549 ]
543 550
544 551 new_keys = shortcut.get("new_keys", None)
545 552 new_filter = shortcut.get("new_filter", None)
546 553
547 command = known_commands[command_id]
554 command = allowed_commands[command_id]
548 555
549 556 creating_new = shortcut.get("create", False)
550 557 modifying_existing = not creating_new and (
551 558 new_keys is not None or new_filter
552 559 )
553 560
554 561 if creating_new and new_keys == []:
555 562 raise ValueError("Cannot add a shortcut without keys")
556 563
557 564 if modifying_existing:
558 565 specification = {
559 566 key: shortcut[key]
560 567 for key in ["command", "filter"]
561 568 if key in shortcut
562 569 }
563 570 if len(matching) == 0:
564 571 raise ValueError(
565 572 f"No shortcuts matching {specification} found in {KEY_BINDINGS}"
566 573 )
567 574 elif len(matching) > 1:
568 575 raise ValueError(
569 576 f"Multiple shortcuts matching {specification} found,"
570 577 f" please add keys/filter to select one of: {matching}"
571 578 )
572 579
573 580 matched = matching[0]
574 581 old_filter = matched.filter
575 582 old_keys = list(matched.keys)
576 583 shortcuts_to_skip.append(
577 584 RuntimeBinding(
578 585 command,
579 586 keys=old_keys,
580 587 filter=old_filter,
581 588 )
582 589 )
583 590
584 591 if new_keys != []:
585 592 shortcuts_to_add.append(
586 593 RuntimeBinding(
587 594 command,
588 595 keys=new_keys or old_keys,
589 filter=filter_from_string(new_filter)
590 if new_filter is not None
591 else (
592 old_filter
593 if old_filter is not None
594 else filter_from_string("always")
596 filter=(
597 filter_from_string(new_filter)
598 if new_filter is not None
599 else (
600 old_filter
601 if old_filter is not None
602 else filter_from_string("always")
603 )
595 604 ),
596 605 )
597 606 )
598 607
599 608 # rebuild the bindings list from scratch
600 609 key_bindings = create_ipython_shortcuts(self, skip=shortcuts_to_skip)
601 610 for binding in shortcuts_to_add:
602 611 add_binding(key_bindings, binding)
603 612
604 613 return key_bindings
605 614
606 615 prompt_includes_vi_mode = Bool(True,
607 616 help="Display the current vi mode (when using vi editing mode)."
608 617 ).tag(config=True)
609 618
610 619 prompt_line_number_format = Unicode(
611 620 "",
612 621 help="The format for line numbering, will be passed `line` (int, 1 based)"
613 622 " the current line number and `rel_line` the relative line number."
614 623 " for example to display both you can use the following template string :"
615 624 " c.TerminalInteractiveShell.prompt_line_number_format='{line: 4d}/{rel_line:+03d} | '"
616 625 " This will display the current line number, with leading space and a width of at least 4"
617 626 " character, as well as the relative line number 0 padded and always with a + or - sign."
618 627 " Note that when using Emacs mode the prompt of the first line may not update.",
619 628 ).tag(config=True)
620 629
621 630 @observe('term_title')
622 631 def init_term_title(self, change=None):
623 632 # Enable or disable the terminal title.
624 633 if self.term_title and _is_tty:
625 634 toggle_set_term_title(True)
626 635 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
627 636 else:
628 637 toggle_set_term_title(False)
629 638
630 639 def restore_term_title(self):
631 640 if self.term_title and _is_tty:
632 641 restore_term_title()
633 642
634 643 def init_display_formatter(self):
635 644 super(TerminalInteractiveShell, self).init_display_formatter()
636 645 # terminal only supports plain text
637 646 self.display_formatter.active_types = ["text/plain"]
638 647
639 648 def init_prompt_toolkit_cli(self):
640 649 if self.simple_prompt:
641 650 # Fall back to plain non-interactive output for tests.
642 651 # This is very limited.
643 652 def prompt():
644 653 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
645 654 lines = [input(prompt_text)]
646 655 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
647 656 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
648 657 lines.append( input(prompt_continuation) )
649 658 return '\n'.join(lines)
650 659 self.prompt_for_code = prompt
651 660 return
652 661
653 662 # Set up keyboard shortcuts
654 663 key_bindings = self._merge_shortcuts(user_shortcuts=self.shortcuts)
655 664
656 665 # Pre-populate history from IPython's history database
657 666 history = PtkHistoryAdapter(self)
658 667
659 668 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
660 669 self.style = DynamicStyle(lambda: self._style)
661 670
662 671 editing_mode = getattr(EditingMode, self.editing_mode.upper())
663 672
664 673 self._use_asyncio_inputhook = False
665 674 self.pt_app = PromptSession(
666 675 auto_suggest=self.auto_suggest,
667 676 editing_mode=editing_mode,
668 677 key_bindings=key_bindings,
669 678 history=history,
670 679 completer=IPythonPTCompleter(shell=self),
671 680 enable_history_search=self.enable_history_search,
672 681 style=self.style,
673 682 include_default_pygments_style=False,
674 683 mouse_support=self.mouse_support,
675 684 enable_open_in_editor=self.extra_open_editor_shortcuts,
676 685 color_depth=self.color_depth,
677 686 tempfile_suffix=".py",
678 687 **self._extra_prompt_options(),
679 688 )
680 689 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
681 690 self.auto_suggest.connect(self.pt_app)
682 691
683 692 def _make_style_from_name_or_cls(self, name_or_cls):
684 693 """
685 694 Small wrapper that make an IPython compatible style from a style name
686 695
687 696 We need that to add style for prompt ... etc.
688 697 """
689 698 style_overrides = {}
690 699 if name_or_cls == 'legacy':
691 700 legacy = self.colors.lower()
692 701 if legacy == 'linux':
693 702 style_cls = get_style_by_name('monokai')
694 703 style_overrides = _style_overrides_linux
695 704 elif legacy == 'lightbg':
696 705 style_overrides = _style_overrides_light_bg
697 706 style_cls = get_style_by_name('pastie')
698 707 elif legacy == 'neutral':
699 708 # The default theme needs to be visible on both a dark background
700 709 # and a light background, because we can't tell what the terminal
701 710 # looks like. These tweaks to the default theme help with that.
702 711 style_cls = get_style_by_name('default')
703 712 style_overrides.update({
704 713 Token.Number: '#ansigreen',
705 714 Token.Operator: 'noinherit',
706 715 Token.String: '#ansiyellow',
707 716 Token.Name.Function: '#ansiblue',
708 717 Token.Name.Class: 'bold #ansiblue',
709 718 Token.Name.Namespace: 'bold #ansiblue',
710 719 Token.Name.Variable.Magic: '#ansiblue',
711 720 Token.Prompt: '#ansigreen',
712 721 Token.PromptNum: '#ansibrightgreen bold',
713 722 Token.OutPrompt: '#ansired',
714 723 Token.OutPromptNum: '#ansibrightred bold',
715 724 })
716 725
717 726 # Hack: Due to limited color support on the Windows console
718 727 # the prompt colors will be wrong without this
719 728 if os.name == 'nt':
720 729 style_overrides.update({
721 730 Token.Prompt: '#ansidarkgreen',
722 731 Token.PromptNum: '#ansigreen bold',
723 732 Token.OutPrompt: '#ansidarkred',
724 733 Token.OutPromptNum: '#ansired bold',
725 734 })
726 735 elif legacy =='nocolor':
727 736 style_cls=_NoStyle
728 737 style_overrides = {}
729 738 else :
730 739 raise ValueError('Got unknown colors: ', legacy)
731 740 else :
732 741 if isinstance(name_or_cls, str):
733 742 style_cls = get_style_by_name(name_or_cls)
734 743 else:
735 744 style_cls = name_or_cls
736 745 style_overrides = {
737 746 Token.Prompt: '#ansigreen',
738 747 Token.PromptNum: '#ansibrightgreen bold',
739 748 Token.OutPrompt: '#ansired',
740 749 Token.OutPromptNum: '#ansibrightred bold',
741 750 }
742 751 style_overrides.update(self.highlighting_style_overrides)
743 752 style = merge_styles([
744 753 style_from_pygments_cls(style_cls),
745 754 style_from_pygments_dict(style_overrides),
746 755 ])
747 756
748 757 return style
749 758
750 759 @property
751 760 def pt_complete_style(self):
752 761 return {
753 762 'multicolumn': CompleteStyle.MULTI_COLUMN,
754 763 'column': CompleteStyle.COLUMN,
755 764 'readlinelike': CompleteStyle.READLINE_LIKE,
756 765 }[self.display_completions]
757 766
758 767 @property
759 768 def color_depth(self):
760 769 return (ColorDepth.TRUE_COLOR if self.true_color else None)
761 770
762 771 def _extra_prompt_options(self):
763 772 """
764 773 Return the current layout option for the current Terminal InteractiveShell
765 774 """
766 775 def get_message():
767 776 return PygmentsTokens(self.prompts.in_prompt_tokens())
768 777
769 778 if self.editing_mode == "emacs" and self.prompt_line_number_format == "":
770 779 # with emacs mode the prompt is (usually) static, so we call only
771 780 # the function once. With VI mode it can toggle between [ins] and
772 781 # [nor] so we can't precompute.
773 782 # here I'm going to favor the default keybinding which almost
774 783 # everybody uses to decrease CPU usage.
775 784 # if we have issues with users with custom Prompts we can see how to
776 785 # work around this.
777 786 get_message = get_message()
778 787
779 788 options = {
780 789 "complete_in_thread": False,
781 790 "lexer": IPythonPTLexer(),
782 791 "reserve_space_for_menu": self.space_for_menu,
783 792 "message": get_message,
784 793 "prompt_continuation": (
785 794 lambda width, lineno, is_soft_wrap: PygmentsTokens(
786 795 _backward_compat_continuation_prompt_tokens(
787 796 self.prompts.continuation_prompt_tokens, width, lineno=lineno
788 797 )
789 798 )
790 799 ),
791 800 "multiline": True,
792 801 "complete_style": self.pt_complete_style,
793 802 "input_processors": [
794 803 # Highlight matching brackets, but only when this setting is
795 804 # enabled, and only when the DEFAULT_BUFFER has the focus.
796 805 ConditionalProcessor(
797 806 processor=HighlightMatchingBracketProcessor(chars="[](){}"),
798 807 filter=HasFocus(DEFAULT_BUFFER)
799 808 & ~IsDone()
800 809 & Condition(lambda: self.highlight_matching_brackets),
801 810 ),
802 811 # Show auto-suggestion in lines other than the last line.
803 812 ConditionalProcessor(
804 813 processor=AppendAutoSuggestionInAnyLine(),
805 814 filter=HasFocus(DEFAULT_BUFFER)
806 815 & ~IsDone()
807 816 & Condition(
808 817 lambda: isinstance(
809 818 self.auto_suggest, NavigableAutoSuggestFromHistory
810 819 )
811 820 ),
812 821 ),
813 822 ],
814 823 }
815 824 if not PTK3:
816 825 options['inputhook'] = self.inputhook
817 826
818 827 return options
819 828
820 829 def prompt_for_code(self):
821 830 if self.rl_next_input:
822 831 default = self.rl_next_input
823 832 self.rl_next_input = None
824 833 else:
825 834 default = ''
826 835
827 836 # In order to make sure that asyncio code written in the
828 837 # interactive shell doesn't interfere with the prompt, we run the
829 838 # prompt in a different event loop.
830 839 # If we don't do this, people could spawn coroutine with a
831 840 # while/true inside which will freeze the prompt.
832 841
833 842 with patch_stdout(raw=True):
834 843 if self._use_asyncio_inputhook:
835 844 # When we integrate the asyncio event loop, run the UI in the
836 845 # same event loop as the rest of the code. don't use an actual
837 846 # input hook. (Asyncio is not made for nesting event loops.)
838 847 asyncio_loop = get_asyncio_loop()
839 848 text = asyncio_loop.run_until_complete(
840 849 self.pt_app.prompt_async(
841 850 default=default, **self._extra_prompt_options()
842 851 )
843 852 )
844 853 else:
845 854 text = self.pt_app.prompt(
846 855 default=default,
847 856 inputhook=self._inputhook,
848 857 **self._extra_prompt_options(),
849 858 )
850 859
851 860 return text
852 861
853 862 def enable_win_unicode_console(self):
854 863 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
855 864 # console by default, so WUC shouldn't be needed.
856 865 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
857 866 DeprecationWarning,
858 867 stacklevel=2)
859 868
860 869 def init_io(self):
861 870 if sys.platform not in {'win32', 'cli'}:
862 871 return
863 872
864 873 import colorama
865 874 colorama.init()
866 875
867 876 def init_magics(self):
868 877 super(TerminalInteractiveShell, self).init_magics()
869 878 self.register_magics(TerminalMagics)
870 879
871 880 def init_alias(self):
872 881 # The parent class defines aliases that can be safely used with any
873 882 # frontend.
874 883 super(TerminalInteractiveShell, self).init_alias()
875 884
876 885 # Now define aliases that only make sense on the terminal, because they
877 886 # need direct access to the console in a way that we can't emulate in
878 887 # GUI or web frontend
879 888 if os.name == 'posix':
880 889 for cmd in ('clear', 'more', 'less', 'man'):
881 890 self.alias_manager.soft_define_alias(cmd, cmd)
882 891
883 892 def __init__(self, *args, **kwargs) -> None:
884 893 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
885 894 self._set_autosuggestions(self.autosuggestions_provider)
886 895 self.init_prompt_toolkit_cli()
887 896 self.init_term_title()
888 897 self.keep_running = True
889 898 self._set_formatter(self.autoformatter)
890 899
891 900 def ask_exit(self):
892 901 self.keep_running = False
893 902
894 903 rl_next_input = None
895 904
896 905 def interact(self):
897 906 self.keep_running = True
898 907 while self.keep_running:
899 908 print(self.separate_in, end='')
900 909
901 910 try:
902 911 code = self.prompt_for_code()
903 912 except EOFError:
904 913 if (not self.confirm_exit) \
905 914 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
906 915 self.ask_exit()
907 916
908 917 else:
909 918 if code:
910 919 self.run_cell(code, store_history=True)
911 920
912 921 def mainloop(self):
913 922 # An extra layer of protection in case someone mashing Ctrl-C breaks
914 923 # out of our internal code.
915 924 while True:
916 925 try:
917 926 self.interact()
918 927 break
919 928 except KeyboardInterrupt as e:
920 929 print("\n%s escaped interact()\n" % type(e).__name__)
921 930 finally:
922 931 # An interrupt during the eventloop will mess up the
923 932 # internal state of the prompt_toolkit library.
924 933 # Stopping the eventloop fixes this, see
925 934 # https://github.com/ipython/ipython/pull/9867
926 935 if hasattr(self, '_eventloop'):
927 936 self._eventloop.stop()
928 937
929 938 self.restore_term_title()
930 939
931 940 # try to call some at-exit operation optimistically as some things can't
932 941 # be done during interpreter shutdown. this is technically inaccurate as
933 942 # this make mainlool not re-callable, but that should be a rare if not
934 943 # in existent use case.
935 944
936 945 self._atexit_once()
937 946
938 947 _inputhook = None
939 948 def inputhook(self, context):
940 949 if self._inputhook is not None:
941 950 self._inputhook(context)
942 951
943 952 active_eventloop: Optional[str] = None
944 953
945 954 def enable_gui(self, gui: Optional[str] = None) -> None:
946 955 if gui:
947 956 from ..core.pylabtools import _convert_gui_from_matplotlib
948 957
949 958 gui = _convert_gui_from_matplotlib(gui)
950 959
951 960 if self.simple_prompt is True and gui is not None:
952 961 print(
953 962 f'Cannot install event loop hook for "{gui}" when running with `--simple-prompt`.'
954 963 )
955 964 print(
956 965 "NOTE: Tk is supported natively; use Tk apps and Tk backends with `--simple-prompt`."
957 966 )
958 967 return
959 968
960 969 if self._inputhook is None and gui is None:
961 970 print("No event loop hook running.")
962 971 return
963 972
964 973 if self._inputhook is not None and gui is not None:
965 974 newev, newinhook = get_inputhook_name_and_func(gui)
966 975 if self._inputhook == newinhook:
967 976 # same inputhook, do nothing
968 977 self.log.info(
969 978 f"Shell is already running the {self.active_eventloop} eventloop. Doing nothing"
970 979 )
971 980 return
972 981 self.log.warning(
973 982 f"Shell is already running a different gui event loop for {self.active_eventloop}. "
974 983 "Call with no arguments to disable the current loop."
975 984 )
976 985 return
977 986 if self._inputhook is not None and gui is None:
978 987 self.active_eventloop = self._inputhook = None
979 988
980 989 if gui and (gui not in {None, "webagg"}):
981 990 # This hook runs with each cycle of the `prompt_toolkit`'s event loop.
982 991 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
983 992 else:
984 993 self.active_eventloop = self._inputhook = None
985 994
986 995 self._use_asyncio_inputhook = gui == "asyncio"
987 996
988 997 # Run !system commands directly, not through pipes, so terminal programs
989 998 # work correctly.
990 999 system = InteractiveShell.system_raw
991 1000
992 1001 def auto_rewrite_input(self, cmd):
993 1002 """Overridden from the parent class to use fancy rewriting prompt"""
994 1003 if not self.show_rewritten_input:
995 1004 return
996 1005
997 1006 tokens = self.prompts.rewrite_prompt_tokens()
998 1007 if self.pt_app:
999 1008 print_formatted_text(PygmentsTokens(tokens), end='',
1000 1009 style=self.pt_app.app.style)
1001 1010 print(cmd)
1002 1011 else:
1003 1012 prompt = ''.join(s for t, s in tokens)
1004 1013 print(prompt, cmd, sep='')
1005 1014
1006 1015 _prompts_before = None
1007 1016 def switch_doctest_mode(self, mode):
1008 1017 """Switch prompts to classic for %doctest_mode"""
1009 1018 if mode:
1010 1019 self._prompts_before = self.prompts
1011 1020 self.prompts = ClassicPrompts(self)
1012 1021 elif self._prompts_before:
1013 1022 self.prompts = self._prompts_before
1014 1023 self._prompts_before = None
1015 1024 # self._update_layout()
1016 1025
1017 1026
1018 1027 InteractiveShellABC.register(TerminalInteractiveShell)
1019 1028
1020 1029 if __name__ == '__main__':
1021 1030 TerminalInteractiveShell.instance().interact()
@@ -1,630 +1,638
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 ["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 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 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 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 268 ["escape", "down"],
269 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 "is_cursor_at_the_end_of_line"
281 281 " & default_buffer_focused"
282 282 " & emacs_like_insert_mode"
283 283 " & pass_through",
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 ]
631
632 UNASSIGNED_ALLOWED_COMMANDS = [
633 nc.beginning_of_buffer,
634 nc.end_of_buffer,
635 nc.end_of_line,
636 nc.forward_word,
637 nc.unix_line_discard,
638 ]
General Comments 0
You need to be logged in to leave comments. Login now