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