##// END OF EJS Templates
reformat with darker
Matthias Bussonnier -
Show More
@@ -1,650 +1,651 b''
1 1 """IPython terminal interface using prompt_toolkit"""
2 2
3 3 import asyncio
4 4 import os
5 5 import sys
6 6 import warnings
7 7 from warnings import warn
8 8
9 9 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
10 10 from IPython.utils import io
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, Unicode, Dict, Integer, observe, Instance, Type, default, Enum, Union,
16 16 Any, validate
17 17 )
18 18
19 19 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
20 20 from prompt_toolkit.filters import (HasFocus, Condition, IsDone)
21 21 from prompt_toolkit.formatted_text import PygmentsTokens
22 22 from prompt_toolkit.history import InMemoryHistory
23 23 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
24 24 from prompt_toolkit.output import ColorDepth
25 25 from prompt_toolkit.patch_stdout import patch_stdout
26 26 from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
27 27 from prompt_toolkit.styles import DynamicStyle, merge_styles
28 28 from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
29 29 from prompt_toolkit import __version__ as ptk_version
30 30
31 31 from pygments.styles import get_style_by_name
32 32 from pygments.style import Style
33 33 from pygments.token import Token
34 34
35 35 from .debugger import TerminalPdb, Pdb
36 36 from .magics import TerminalMagics
37 37 from .pt_inputhooks import get_inputhook_name_and_func
38 38 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
39 39 from .ptutils import IPythonPTCompleter, IPythonPTLexer
40 40 from .shortcuts import create_ipython_shortcuts
41 41
42 42 DISPLAY_BANNER_DEPRECATED = object()
43 43 PTK3 = ptk_version.startswith('3.')
44 44
45 45
46 46 class _NoStyle(Style): pass
47 47
48 48
49 49
50 50 _style_overrides_light_bg = {
51 51 Token.Prompt: '#ansibrightblue',
52 52 Token.PromptNum: '#ansiblue bold',
53 53 Token.OutPrompt: '#ansibrightred',
54 54 Token.OutPromptNum: '#ansired bold',
55 55 }
56 56
57 57 _style_overrides_linux = {
58 58 Token.Prompt: '#ansibrightgreen',
59 59 Token.PromptNum: '#ansigreen bold',
60 60 Token.OutPrompt: '#ansibrightred',
61 61 Token.OutPromptNum: '#ansired bold',
62 62 }
63 63
64 64 def get_default_editor():
65 65 try:
66 66 return os.environ['EDITOR']
67 67 except KeyError:
68 68 pass
69 69 except UnicodeError:
70 70 warn("$EDITOR environment variable is not pure ASCII. Using platform "
71 71 "default editor.")
72 72
73 73 if os.name == 'posix':
74 74 return 'vi' # the only one guaranteed to be there!
75 75 else:
76 76 return 'notepad' # same in Windows!
77 77
78 78 # conservatively check for tty
79 79 # overridden streams can result in things like:
80 80 # - sys.stdin = None
81 81 # - no isatty method
82 82 for _name in ('stdin', 'stdout', 'stderr'):
83 83 _stream = getattr(sys, _name)
84 84 if not _stream or not hasattr(_stream, 'isatty') or not _stream.isatty():
85 85 _is_tty = False
86 86 break
87 87 else:
88 88 _is_tty = True
89 89
90 90
91 91 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
92 92
93 93 def black_reformat_handler(text_before_cursor):
94 94 import black
95 95 formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
96 96 if not text_before_cursor.endswith('\n') and formatted_text.endswith('\n'):
97 97 formatted_text = formatted_text[:-1]
98 98 return formatted_text
99 99
100 100
101 101 class TerminalInteractiveShell(InteractiveShell):
102 102 mime_renderers = Dict().tag(config=True)
103 103
104 104 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
105 105 'to reserve for the tab completion menu, '
106 106 'search history, ...etc, the height of '
107 107 'these menus will at most this value. '
108 108 'Increase it is you prefer long and skinny '
109 109 'menus, decrease for short and wide.'
110 110 ).tag(config=True)
111 111
112 112 pt_app = None
113 113 debugger_history = None
114 114
115 115 simple_prompt = Bool(_use_simple_prompt,
116 116 help="""Use `raw_input` for the REPL, without completion and prompt colors.
117 117
118 118 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
119 119 IPython own testing machinery, and emacs inferior-shell integration through elpy.
120 120
121 121 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
122 122 environment variable is set, or the current terminal is not a tty."""
123 123 ).tag(config=True)
124 124
125 125 @property
126 126 def debugger_cls(self):
127 127 return Pdb if self.simple_prompt else TerminalPdb
128 128
129 129 confirm_exit = Bool(True,
130 130 help="""
131 131 Set to confirm when you try to exit IPython with an EOF (Control-D
132 132 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
133 133 you can force a direct exit without any confirmation.""",
134 134 ).tag(config=True)
135 135
136 136 editing_mode = Unicode('emacs',
137 137 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
138 138 ).tag(config=True)
139 139
140 emacs_bindings_in_vi_insert_mode = Bool(True,
140 emacs_bindings_in_vi_insert_mode = Bool(
141 True,
141 142 help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.",
142 143 ).tag(config=True)
143 144
144 145 autoformatter = Unicode(None,
145 146 help="Autoformatter to reformat Terminal code. Can be `'black'` or `None`",
146 147 allow_none=True
147 148 ).tag(config=True)
148 149
149 150 mouse_support = Bool(False,
150 151 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
151 152 ).tag(config=True)
152 153
153 154 # We don't load the list of styles for the help string, because loading
154 155 # Pygments plugins takes time and can cause unexpected errors.
155 156 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
156 157 help="""The name or class of a Pygments style to use for syntax
157 158 highlighting. To see available styles, run `pygmentize -L styles`."""
158 159 ).tag(config=True)
159 160
160 161 @validate('editing_mode')
161 162 def _validate_editing_mode(self, proposal):
162 163 if proposal['value'].lower() == 'vim':
163 164 proposal['value']= 'vi'
164 165 elif proposal['value'].lower() == 'default':
165 166 proposal['value']= 'emacs'
166 167
167 168 if hasattr(EditingMode, proposal['value'].upper()):
168 169 return proposal['value'].lower()
169 170
170 171 return self.editing_mode
171 172
172 173
173 174 @observe('editing_mode')
174 175 def _editing_mode(self, change):
175 176 u_mode = change.new.upper()
176 177 if self.pt_app:
177 178 self.pt_app.editing_mode = u_mode
178 179
179 180 @observe('autoformatter')
180 181 def _autoformatter_changed(self, change):
181 182 formatter = change.new
182 183 if formatter is None:
183 184 self.reformat_handler = lambda x:x
184 185 elif formatter == 'black':
185 186 self.reformat_handler = black_reformat_handler
186 187 else:
187 188 raise ValueError
188 189
189 190 @observe('highlighting_style')
190 191 @observe('colors')
191 192 def _highlighting_style_changed(self, change):
192 193 self.refresh_style()
193 194
194 195 def refresh_style(self):
195 196 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
196 197
197 198
198 199 highlighting_style_overrides = Dict(
199 200 help="Override highlighting format for specific tokens"
200 201 ).tag(config=True)
201 202
202 203 true_color = Bool(False,
203 204 help=("Use 24bit colors instead of 256 colors in prompt highlighting. "
204 205 "If your terminal supports true color, the following command "
205 206 "should print 'TRUECOLOR' in orange: "
206 207 "printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"")
207 208 ).tag(config=True)
208 209
209 210 editor = Unicode(get_default_editor(),
210 211 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
211 212 ).tag(config=True)
212 213
213 214 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
214 215
215 216 prompts = Instance(Prompts)
216 217
217 218 @default('prompts')
218 219 def _prompts_default(self):
219 220 return self.prompts_class(self)
220 221
221 222 # @observe('prompts')
222 223 # def _(self, change):
223 224 # self._update_layout()
224 225
225 226 @default('displayhook_class')
226 227 def _displayhook_class_default(self):
227 228 return RichPromptDisplayHook
228 229
229 230 term_title = Bool(True,
230 231 help="Automatically set the terminal title"
231 232 ).tag(config=True)
232 233
233 234 term_title_format = Unicode("IPython: {cwd}",
234 235 help="Customize the terminal title format. This is a python format string. " +
235 236 "Available substitutions are: {cwd}."
236 237 ).tag(config=True)
237 238
238 239 display_completions = Enum(('column', 'multicolumn','readlinelike'),
239 240 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
240 241 "'readlinelike'. These options are for `prompt_toolkit`, see "
241 242 "`prompt_toolkit` documentation for more information."
242 243 ),
243 244 default_value='multicolumn').tag(config=True)
244 245
245 246 highlight_matching_brackets = Bool(True,
246 247 help="Highlight matching brackets.",
247 248 ).tag(config=True)
248 249
249 250 extra_open_editor_shortcuts = Bool(False,
250 251 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
251 252 "This is in addition to the F2 binding, which is always enabled."
252 253 ).tag(config=True)
253 254
254 255 handle_return = Any(None,
255 256 help="Provide an alternative handler to be called when the user presses "
256 257 "Return. This is an advanced option intended for debugging, which "
257 258 "may be changed or removed in later releases."
258 259 ).tag(config=True)
259 260
260 261 enable_history_search = Bool(True,
261 262 help="Allows to enable/disable the prompt toolkit history search"
262 263 ).tag(config=True)
263 264
264 265 prompt_includes_vi_mode = Bool(True,
265 266 help="Display the current vi mode (when using vi editing mode)."
266 267 ).tag(config=True)
267 268
268 269 @observe('term_title')
269 270 def init_term_title(self, change=None):
270 271 # Enable or disable the terminal title.
271 272 if self.term_title:
272 273 toggle_set_term_title(True)
273 274 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
274 275 else:
275 276 toggle_set_term_title(False)
276 277
277 278 def restore_term_title(self):
278 279 if self.term_title:
279 280 restore_term_title()
280 281
281 282 def init_display_formatter(self):
282 283 super(TerminalInteractiveShell, self).init_display_formatter()
283 284 # terminal only supports plain text
284 285 self.display_formatter.active_types = ['text/plain']
285 286 # disable `_ipython_display_`
286 287 self.display_formatter.ipython_display_formatter.enabled = False
287 288
288 289 def init_prompt_toolkit_cli(self):
289 290 if self.simple_prompt:
290 291 # Fall back to plain non-interactive output for tests.
291 292 # This is very limited.
292 293 def prompt():
293 294 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
294 295 lines = [input(prompt_text)]
295 296 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
296 297 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
297 298 lines.append( input(prompt_continuation) )
298 299 return '\n'.join(lines)
299 300 self.prompt_for_code = prompt
300 301 return
301 302
302 303 # Set up keyboard shortcuts
303 304 key_bindings = create_ipython_shortcuts(self)
304 305
305 306 # Pre-populate history from IPython's history database
306 307 history = InMemoryHistory()
307 308 last_cell = u""
308 309 for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
309 310 include_latest=True):
310 311 # Ignore blank lines and consecutive duplicates
311 312 cell = cell.rstrip()
312 313 if cell and (cell != last_cell):
313 314 history.append_string(cell)
314 315 last_cell = cell
315 316
316 317 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
317 318 self.style = DynamicStyle(lambda: self._style)
318 319
319 320 editing_mode = getattr(EditingMode, self.editing_mode.upper())
320 321
321 322 self.pt_loop = asyncio.new_event_loop()
322 323 self.pt_app = PromptSession(
323 324 editing_mode=editing_mode,
324 325 key_bindings=key_bindings,
325 326 history=history,
326 327 completer=IPythonPTCompleter(shell=self),
327 328 enable_history_search = self.enable_history_search,
328 329 style=self.style,
329 330 include_default_pygments_style=False,
330 331 mouse_support=self.mouse_support,
331 332 enable_open_in_editor=self.extra_open_editor_shortcuts,
332 333 color_depth=self.color_depth,
333 334 tempfile_suffix=".py",
334 335 **self._extra_prompt_options())
335 336
336 337 def _make_style_from_name_or_cls(self, name_or_cls):
337 338 """
338 339 Small wrapper that make an IPython compatible style from a style name
339 340
340 341 We need that to add style for prompt ... etc.
341 342 """
342 343 style_overrides = {}
343 344 if name_or_cls == 'legacy':
344 345 legacy = self.colors.lower()
345 346 if legacy == 'linux':
346 347 style_cls = get_style_by_name('monokai')
347 348 style_overrides = _style_overrides_linux
348 349 elif legacy == 'lightbg':
349 350 style_overrides = _style_overrides_light_bg
350 351 style_cls = get_style_by_name('pastie')
351 352 elif legacy == 'neutral':
352 353 # The default theme needs to be visible on both a dark background
353 354 # and a light background, because we can't tell what the terminal
354 355 # looks like. These tweaks to the default theme help with that.
355 356 style_cls = get_style_by_name('default')
356 357 style_overrides.update({
357 358 Token.Number: '#ansigreen',
358 359 Token.Operator: 'noinherit',
359 360 Token.String: '#ansiyellow',
360 361 Token.Name.Function: '#ansiblue',
361 362 Token.Name.Class: 'bold #ansiblue',
362 363 Token.Name.Namespace: 'bold #ansiblue',
363 364 Token.Name.Variable.Magic: '#ansiblue',
364 365 Token.Prompt: '#ansigreen',
365 366 Token.PromptNum: '#ansibrightgreen bold',
366 367 Token.OutPrompt: '#ansired',
367 368 Token.OutPromptNum: '#ansibrightred bold',
368 369 })
369 370
370 371 # Hack: Due to limited color support on the Windows console
371 372 # the prompt colors will be wrong without this
372 373 if os.name == 'nt':
373 374 style_overrides.update({
374 375 Token.Prompt: '#ansidarkgreen',
375 376 Token.PromptNum: '#ansigreen bold',
376 377 Token.OutPrompt: '#ansidarkred',
377 378 Token.OutPromptNum: '#ansired bold',
378 379 })
379 380 elif legacy =='nocolor':
380 381 style_cls=_NoStyle
381 382 style_overrides = {}
382 383 else :
383 384 raise ValueError('Got unknown colors: ', legacy)
384 385 else :
385 386 if isinstance(name_or_cls, str):
386 387 style_cls = get_style_by_name(name_or_cls)
387 388 else:
388 389 style_cls = name_or_cls
389 390 style_overrides = {
390 391 Token.Prompt: '#ansigreen',
391 392 Token.PromptNum: '#ansibrightgreen bold',
392 393 Token.OutPrompt: '#ansired',
393 394 Token.OutPromptNum: '#ansibrightred bold',
394 395 }
395 396 style_overrides.update(self.highlighting_style_overrides)
396 397 style = merge_styles([
397 398 style_from_pygments_cls(style_cls),
398 399 style_from_pygments_dict(style_overrides),
399 400 ])
400 401
401 402 return style
402 403
403 404 @property
404 405 def pt_complete_style(self):
405 406 return {
406 407 'multicolumn': CompleteStyle.MULTI_COLUMN,
407 408 'column': CompleteStyle.COLUMN,
408 409 'readlinelike': CompleteStyle.READLINE_LIKE,
409 410 }[self.display_completions]
410 411
411 412 @property
412 413 def color_depth(self):
413 414 return (ColorDepth.TRUE_COLOR if self.true_color else None)
414 415
415 416 def _extra_prompt_options(self):
416 417 """
417 418 Return the current layout option for the current Terminal InteractiveShell
418 419 """
419 420 def get_message():
420 421 return PygmentsTokens(self.prompts.in_prompt_tokens())
421 422
422 423 if self.editing_mode == 'emacs':
423 424 # with emacs mode the prompt is (usually) static, so we call only
424 425 # the function once. With VI mode it can toggle between [ins] and
425 426 # [nor] so we can't precompute.
426 427 # here I'm going to favor the default keybinding which almost
427 428 # everybody uses to decrease CPU usage.
428 429 # if we have issues with users with custom Prompts we can see how to
429 430 # work around this.
430 431 get_message = get_message()
431 432
432 433 options = {
433 434 'complete_in_thread': False,
434 435 'lexer':IPythonPTLexer(),
435 436 'reserve_space_for_menu':self.space_for_menu,
436 437 'message': get_message,
437 438 'prompt_continuation': (
438 439 lambda width, lineno, is_soft_wrap:
439 440 PygmentsTokens(self.prompts.continuation_prompt_tokens(width))),
440 441 'multiline': True,
441 442 'complete_style': self.pt_complete_style,
442 443
443 444 # Highlight matching brackets, but only when this setting is
444 445 # enabled, and only when the DEFAULT_BUFFER has the focus.
445 446 'input_processors': [ConditionalProcessor(
446 447 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
447 448 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
448 449 Condition(lambda: self.highlight_matching_brackets))],
449 450 }
450 451 if not PTK3:
451 452 options['inputhook'] = self.inputhook
452 453
453 454 return options
454 455
455 456 def prompt_for_code(self):
456 457 if self.rl_next_input:
457 458 default = self.rl_next_input
458 459 self.rl_next_input = None
459 460 else:
460 461 default = ''
461 462
462 463 # In order to make sure that asyncio code written in the
463 464 # interactive shell doesn't interfere with the prompt, we run the
464 465 # prompt in a different event loop.
465 466 # If we don't do this, people could spawn coroutine with a
466 467 # while/true inside which will freeze the prompt.
467 468
468 469 try:
469 470 old_loop = asyncio.get_event_loop()
470 471 except RuntimeError:
471 472 # This happens when the user used `asyncio.run()`.
472 473 old_loop = None
473 474
474 475 asyncio.set_event_loop(self.pt_loop)
475 476 try:
476 477 with patch_stdout(raw=True):
477 478 text = self.pt_app.prompt(
478 479 default=default,
479 480 **self._extra_prompt_options())
480 481 finally:
481 482 # Restore the original event loop.
482 483 asyncio.set_event_loop(old_loop)
483 484
484 485 return text
485 486
486 487 def enable_win_unicode_console(self):
487 488 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
488 489 # console by default, so WUC shouldn't be needed.
489 490 from warnings import warn
490 491 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
491 492 DeprecationWarning,
492 493 stacklevel=2)
493 494
494 495 def init_io(self):
495 496 if sys.platform not in {'win32', 'cli'}:
496 497 return
497 498
498 499 import colorama
499 500 colorama.init()
500 501
501 502 # For some reason we make these wrappers around stdout/stderr.
502 503 # For now, we need to reset them so all output gets coloured.
503 504 # https://github.com/ipython/ipython/issues/8669
504 505 # io.std* are deprecated, but don't show our own deprecation warnings
505 506 # during initialization of the deprecated API.
506 507 with warnings.catch_warnings():
507 508 warnings.simplefilter('ignore', DeprecationWarning)
508 509 io.stdout = io.IOStream(sys.stdout)
509 510 io.stderr = io.IOStream(sys.stderr)
510 511
511 512 def init_magics(self):
512 513 super(TerminalInteractiveShell, self).init_magics()
513 514 self.register_magics(TerminalMagics)
514 515
515 516 def init_alias(self):
516 517 # The parent class defines aliases that can be safely used with any
517 518 # frontend.
518 519 super(TerminalInteractiveShell, self).init_alias()
519 520
520 521 # Now define aliases that only make sense on the terminal, because they
521 522 # need direct access to the console in a way that we can't emulate in
522 523 # GUI or web frontend
523 524 if os.name == 'posix':
524 525 for cmd in ('clear', 'more', 'less', 'man'):
525 526 self.alias_manager.soft_define_alias(cmd, cmd)
526 527
527 528
528 529 def __init__(self, *args, **kwargs):
529 530 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
530 531 self.init_prompt_toolkit_cli()
531 532 self.init_term_title()
532 533 self.keep_running = True
533 534
534 535 self.debugger_history = InMemoryHistory()
535 536
536 537 def ask_exit(self):
537 538 self.keep_running = False
538 539
539 540 rl_next_input = None
540 541
541 542 def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):
542 543
543 544 if display_banner is not DISPLAY_BANNER_DEPRECATED:
544 545 warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
545 546
546 547 self.keep_running = True
547 548 while self.keep_running:
548 549 print(self.separate_in, end='')
549 550
550 551 try:
551 552 code = self.prompt_for_code()
552 553 except EOFError:
553 554 if (not self.confirm_exit) \
554 555 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
555 556 self.ask_exit()
556 557
557 558 else:
558 559 if code:
559 560 self.run_cell(code, store_history=True)
560 561
561 562 def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED):
562 563 # An extra layer of protection in case someone mashing Ctrl-C breaks
563 564 # out of our internal code.
564 565 if display_banner is not DISPLAY_BANNER_DEPRECATED:
565 566 warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
566 567 while True:
567 568 try:
568 569 self.interact()
569 570 break
570 571 except KeyboardInterrupt as e:
571 572 print("\n%s escaped interact()\n" % type(e).__name__)
572 573 finally:
573 574 # An interrupt during the eventloop will mess up the
574 575 # internal state of the prompt_toolkit library.
575 576 # Stopping the eventloop fixes this, see
576 577 # https://github.com/ipython/ipython/pull/9867
577 578 if hasattr(self, '_eventloop'):
578 579 self._eventloop.stop()
579 580
580 581 self.restore_term_title()
581 582
582 583
583 584 _inputhook = None
584 585 def inputhook(self, context):
585 586 if self._inputhook is not None:
586 587 self._inputhook(context)
587 588
588 589 active_eventloop = None
589 590 def enable_gui(self, gui=None):
590 591 if gui and (gui != 'inline') :
591 592 self.active_eventloop, self._inputhook =\
592 593 get_inputhook_name_and_func(gui)
593 594 else:
594 595 self.active_eventloop = self._inputhook = None
595 596
596 597 # For prompt_toolkit 3.0. We have to create an asyncio event loop with
597 598 # this inputhook.
598 599 if PTK3:
599 600 import asyncio
600 601 from prompt_toolkit.eventloop import new_eventloop_with_inputhook
601 602
602 603 if gui == 'asyncio':
603 604 # When we integrate the asyncio event loop, run the UI in the
604 605 # same event loop as the rest of the code. don't use an actual
605 606 # input hook. (Asyncio is not made for nesting event loops.)
606 607 self.pt_loop = asyncio.get_event_loop()
607 608
608 609 elif self._inputhook:
609 610 # If an inputhook was set, create a new asyncio event loop with
610 611 # this inputhook for the prompt.
611 612 self.pt_loop = new_eventloop_with_inputhook(self._inputhook)
612 613 else:
613 614 # When there's no inputhook, run the prompt in a separate
614 615 # asyncio event loop.
615 616 self.pt_loop = asyncio.new_event_loop()
616 617
617 618 # Run !system commands directly, not through pipes, so terminal programs
618 619 # work correctly.
619 620 system = InteractiveShell.system_raw
620 621
621 622 def auto_rewrite_input(self, cmd):
622 623 """Overridden from the parent class to use fancy rewriting prompt"""
623 624 if not self.show_rewritten_input:
624 625 return
625 626
626 627 tokens = self.prompts.rewrite_prompt_tokens()
627 628 if self.pt_app:
628 629 print_formatted_text(PygmentsTokens(tokens), end='',
629 630 style=self.pt_app.app.style)
630 631 print(cmd)
631 632 else:
632 633 prompt = ''.join(s for t, s in tokens)
633 634 print(prompt, cmd, sep='')
634 635
635 636 _prompts_before = None
636 637 def switch_doctest_mode(self, mode):
637 638 """Switch prompts to classic for %doctest_mode"""
638 639 if mode:
639 640 self._prompts_before = self.prompts
640 641 self.prompts = ClassicPrompts(self)
641 642 elif self._prompts_before:
642 643 self.prompts = self._prompts_before
643 644 self._prompts_before = None
644 645 # self._update_layout()
645 646
646 647
647 648 InteractiveShellABC.register(TerminalInteractiveShell)
648 649
649 650 if __name__ == '__main__':
650 651 TerminalInteractiveShell.instance().interact()
@@ -1,345 +1,344 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 warnings
10 10 import signal
11 11 import sys
12 12 from typing import Callable
13 13
14 14
15 15 from prompt_toolkit.application.current import get_app
16 16 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
17 17 from prompt_toolkit.filters import (has_focus, has_selection, Condition,
18 18 vi_insert_mode, emacs_insert_mode, has_completions, vi_mode)
19 19 from prompt_toolkit.key_binding.bindings.completion import display_completions_like_readline
20 20 from prompt_toolkit.key_binding import KeyBindings
21 21 from prompt_toolkit.key_binding.bindings import named_commands as nc
22 22
23 23 from IPython.utils.decorators import undoc
24 24
25 25 @undoc
26 26 @Condition
27 27 def cursor_in_leading_ws():
28 28 before = get_app().current_buffer.document.current_line_before_cursor
29 29 return (not before) or before.isspace()
30 30
31 31
32 32 def create_ipython_shortcuts(shell):
33 33 """Set up the prompt_toolkit keyboard shortcuts for IPython"""
34 34
35 35 kb = KeyBindings()
36 36 insert_mode = vi_insert_mode | emacs_insert_mode
37 37
38 38 if getattr(shell, 'handle_return', None):
39 39 return_handler = shell.handle_return(shell)
40 40 else:
41 41 return_handler = newline_or_execute_outer(shell)
42 42
43 43 kb.add('enter', filter=(has_focus(DEFAULT_BUFFER)
44 44 & ~has_selection
45 45 & insert_mode
46 46 ))(return_handler)
47 47
48 48 def reformat_and_execute(event):
49 49 reformat_text_before_cursor(event.current_buffer, event.current_buffer.document, shell)
50 50 event.current_buffer.validate_and_handle()
51 51
52 52 kb.add('escape', 'enter', filter=(has_focus(DEFAULT_BUFFER)
53 53 & ~has_selection
54 54 & insert_mode
55 55 ))(reformat_and_execute)
56 56
57 57 kb.add('c-\\')(force_exit)
58 58
59 59 kb.add('c-p', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER))
60 60 )(previous_history_or_previous_completion)
61 61
62 62 kb.add('c-n', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER))
63 63 )(next_history_or_next_completion)
64 64
65 65 kb.add('c-g', filter=(has_focus(DEFAULT_BUFFER) & has_completions)
66 66 )(dismiss_completion)
67 67
68 68 kb.add('c-c', filter=has_focus(DEFAULT_BUFFER))(reset_buffer)
69 69
70 70 kb.add('c-c', filter=has_focus(SEARCH_BUFFER))(reset_search_buffer)
71 71
72 72 supports_suspend = Condition(lambda: hasattr(signal, 'SIGTSTP'))
73 73 kb.add('c-z', filter=supports_suspend)(suspend_to_bg)
74 74
75 75 # Ctrl+I == Tab
76 76 kb.add('tab', filter=(has_focus(DEFAULT_BUFFER)
77 77 & ~has_selection
78 78 & insert_mode
79 79 & cursor_in_leading_ws
80 80 ))(indent_buffer)
81 81 kb.add('c-o', filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode)
82 82 )(newline_autoindent_outer(shell.input_transformer_manager))
83 83
84 84 kb.add('f2', filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor)
85 85
86 86 if shell.display_completions == 'readlinelike':
87 87 kb.add('c-i', filter=(has_focus(DEFAULT_BUFFER)
88 88 & ~has_selection
89 89 & insert_mode
90 90 & ~cursor_in_leading_ws
91 91 ))(display_completions_like_readline)
92 92
93 if sys.platform == 'win32':
94 kb.add('c-v', filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste)
95
93 if sys.platform == "win32":
94 kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste)
96 95
97 96 @Condition
98 97 def ebivim():
99 98 return shell.emacs_bindings_in_vi_insert_mode
100 99
101 100 focused_insert = has_focus(DEFAULT_BUFFER) & vi_insert_mode
102 101
103 102 # Needed for to accept autosuggestions in vi insert mode
104 103 @kb.add("c-e", filter=focused_insert & ebivim)
105 104 def _(event):
106 105 b = event.current_buffer
107 106 suggestion = b.suggestion
108 107 if suggestion:
109 108 b.insert_text(suggestion.text)
110 109 else:
111 110 nc.end_of_line(event)
112 111
113 112 @kb.add("c-f", filter=focused_insert & ebivim)
114 113 def _(event):
115 114 b = event.current_buffer
116 115 suggestion = b.suggestion
117 116 if suggestion:
118 117 b.insert_text(suggestion.text)
119 118 else:
120 119 nc.forward_char(event)
121 120
122 121 @kb.add("escape", "f", filter=focused_insert & ebivim)
123 122 def _(event):
124 123 b = event.current_buffer
125 124 suggestion = b.suggestion
126 125 if suggestion:
127 126 t = re.split(r"(\S+\s+)", suggestion.text)
128 127 b.insert_text(next((x for x in t if x), ""))
129 128 else:
130 129 nc.forward_word(event)
131 130
132 131 # Simple Control keybindings
133 132 key_cmd_dict = {
134 133 "c-a": nc.beginning_of_line,
135 134 "c-b": nc.backward_char,
136 135 "c-k": nc.kill_line,
137 136 "c-w": nc.backward_kill_word,
138 137 "c-y": nc.yank,
139 138 "c-_": nc.undo,
140 139 }
141 140
142 141 for key, cmd in key_cmd_dict.items():
143 142 kb.add(key, filter=focused_insert & ebivim)(cmd)
144 143
145 144 # Alt and Combo Control keybindings
146 145 keys_cmd_dict = {
147 146 # Control Combos
148 147 ("c-x", "c-e"): nc.edit_and_execute,
149 148 ("c-x", "e"): nc.edit_and_execute,
150 149 # Alt
151 150 ("escape", "b"): nc.backward_word,
152 151 ("escape", "c"): nc.capitalize_word,
153 152 ("escape", "d"): nc.kill_word,
154 153 ("escape", "h"): nc.backward_kill_word,
155 154 ("escape", "l"): nc.downcase_word,
156 155 ("escape", "u"): nc.uppercase_word,
157 156 ("escape", "y"): nc.yank_pop,
158 157 ("escape", "."): nc.yank_last_arg,
159 158 }
160 159
161 160 for keys, cmd in keys_cmd_dict.items():
162 161 kb.add(*keys, filter=focused_insert & ebivim)(cmd)
163 162
164 163 return kb
165 164
166 165
167 166 def reformat_text_before_cursor(buffer, document, shell):
168 167 text = buffer.delete_before_cursor(len(document.text[:document.cursor_position]))
169 168 try:
170 169 formatted_text = shell.reformat_handler(text)
171 170 buffer.insert_text(formatted_text)
172 171 except Exception as e:
173 172 buffer.insert_text(text)
174 173
175 174
176 175 def newline_or_execute_outer(shell):
177 176
178 177 def newline_or_execute(event):
179 178 """When the user presses return, insert a newline or execute the code."""
180 179 b = event.current_buffer
181 180 d = b.document
182 181
183 182 if b.complete_state:
184 183 cc = b.complete_state.current_completion
185 184 if cc:
186 185 b.apply_completion(cc)
187 186 else:
188 187 b.cancel_completion()
189 188 return
190 189
191 190 # If there's only one line, treat it as if the cursor is at the end.
192 191 # See https://github.com/ipython/ipython/issues/10425
193 192 if d.line_count == 1:
194 193 check_text = d.text
195 194 else:
196 195 check_text = d.text[:d.cursor_position]
197 196 status, indent = shell.check_complete(check_text)
198 197
199 198 # if all we have after the cursor is whitespace: reformat current text
200 199 # before cursor
201 200 after_cursor = d.text[d.cursor_position:]
202 201 reformatted = False
203 202 if not after_cursor.strip():
204 203 reformat_text_before_cursor(b, d, shell)
205 204 reformatted = True
206 205 if not (d.on_last_line or
207 206 d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
208 207 ):
209 208 if shell.autoindent:
210 209 b.insert_text('\n' + indent)
211 210 else:
212 211 b.insert_text('\n')
213 212 return
214 213
215 214 if (status != 'incomplete') and b.accept_handler:
216 215 if not reformatted:
217 216 reformat_text_before_cursor(b, d, shell)
218 217 b.validate_and_handle()
219 218 else:
220 219 if shell.autoindent:
221 220 b.insert_text('\n' + indent)
222 221 else:
223 222 b.insert_text('\n')
224 223 return newline_or_execute
225 224
226 225
227 226 def previous_history_or_previous_completion(event):
228 227 """
229 228 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
230 229
231 230 If completer is open this still select previous completion.
232 231 """
233 232 event.current_buffer.auto_up()
234 233
235 234
236 235 def next_history_or_next_completion(event):
237 236 """
238 237 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
239 238
240 239 If completer is open this still select next completion.
241 240 """
242 241 event.current_buffer.auto_down()
243 242
244 243
245 244 def dismiss_completion(event):
246 245 b = event.current_buffer
247 246 if b.complete_state:
248 247 b.cancel_completion()
249 248
250 249
251 250 def reset_buffer(event):
252 251 b = event.current_buffer
253 252 if b.complete_state:
254 253 b.cancel_completion()
255 254 else:
256 255 b.reset()
257 256
258 257
259 258 def reset_search_buffer(event):
260 259 if event.current_buffer.document.text:
261 260 event.current_buffer.reset()
262 261 else:
263 262 event.app.layout.focus(DEFAULT_BUFFER)
264 263
265 264 def suspend_to_bg(event):
266 265 event.app.suspend_to_background()
267 266
268 267 def force_exit(event):
269 268 """
270 269 Force exit (with a non-zero return value)
271 270 """
272 271 sys.exit("Quit")
273 272
274 273 def indent_buffer(event):
275 274 event.current_buffer.insert_text(' ' * 4)
276 275
277 276 @undoc
278 277 def newline_with_copy_margin(event):
279 278 """
280 279 DEPRECATED since IPython 6.0
281 280
282 281 See :any:`newline_autoindent_outer` for a replacement.
283 282
284 283 Preserve margin and cursor position when using
285 284 Control-O to insert a newline in EMACS mode
286 285 """
287 286 warnings.warn("`newline_with_copy_margin(event)` is deprecated since IPython 6.0. "
288 287 "see `newline_autoindent_outer(shell)(event)` for a replacement.",
289 288 DeprecationWarning, stacklevel=2)
290 289
291 290 b = event.current_buffer
292 291 cursor_start_pos = b.document.cursor_position_col
293 292 b.newline(copy_margin=True)
294 293 b.cursor_up(count=1)
295 294 cursor_end_pos = b.document.cursor_position_col
296 295 if cursor_start_pos != cursor_end_pos:
297 296 pos_diff = cursor_start_pos - cursor_end_pos
298 297 b.cursor_right(count=pos_diff)
299 298
300 299 def newline_autoindent_outer(inputsplitter) -> Callable[..., None]:
301 300 """
302 301 Return a function suitable for inserting a indented newline after the cursor.
303 302
304 303 Fancier version of deprecated ``newline_with_copy_margin`` which should
305 304 compute the correct indentation of the inserted line. That is to say, indent
306 305 by 4 extra space after a function definition, class definition, context
307 306 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
308 307 """
309 308
310 309 def newline_autoindent(event):
311 310 """insert a newline after the cursor indented appropriately."""
312 311 b = event.current_buffer
313 312 d = b.document
314 313
315 314 if b.complete_state:
316 315 b.cancel_completion()
317 316 text = d.text[:d.cursor_position] + '\n'
318 317 _, indent = inputsplitter.check_complete(text)
319 318 b.insert_text('\n' + (' ' * (indent or 0)), move_cursor=False)
320 319
321 320 return newline_autoindent
322 321
323 322
324 323 def open_input_in_editor(event):
325 324 event.app.current_buffer.open_in_editor()
326 325
327 326
328 327 if sys.platform == 'win32':
329 328 from IPython.core.error import TryNext
330 329 from IPython.lib.clipboard import (ClipboardEmpty,
331 330 win32_clipboard_get,
332 331 tkinter_clipboard_get)
333 332
334 333 @undoc
335 334 def win_paste(event):
336 335 try:
337 336 text = win32_clipboard_get()
338 337 except TryNext:
339 338 try:
340 339 text = tkinter_clipboard_get()
341 340 except (TryNext, ClipboardEmpty):
342 341 return
343 342 except ClipboardEmpty:
344 343 return
345 344 event.current_buffer.insert_text(text.replace('\t', ' ' * 4))
General Comments 0
You need to be logged in to leave comments. Login now