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