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