##// END OF EJS Templates
cursor shape changes with vi editing mode
Martin Skarzynski -
Show More
@@ -1,651 +1,681 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 "
206 236 "should print 'TRUECOLOR' in orange: "
207 237 "printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"")
208 238 ).tag(config=True)
209 239
210 240 editor = Unicode(get_default_editor(),
211 241 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
212 242 ).tag(config=True)
213 243
214 244 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
215 245
216 246 prompts = Instance(Prompts)
217 247
218 248 @default('prompts')
219 249 def _prompts_default(self):
220 250 return self.prompts_class(self)
221 251
222 252 # @observe('prompts')
223 253 # def _(self, change):
224 254 # self._update_layout()
225 255
226 256 @default('displayhook_class')
227 257 def _displayhook_class_default(self):
228 258 return RichPromptDisplayHook
229 259
230 260 term_title = Bool(True,
231 261 help="Automatically set the terminal title"
232 262 ).tag(config=True)
233 263
234 264 term_title_format = Unicode("IPython: {cwd}",
235 265 help="Customize the terminal title format. This is a python format string. " +
236 266 "Available substitutions are: {cwd}."
237 267 ).tag(config=True)
238 268
239 269 display_completions = Enum(('column', 'multicolumn','readlinelike'),
240 270 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
241 271 "'readlinelike'. These options are for `prompt_toolkit`, see "
242 272 "`prompt_toolkit` documentation for more information."
243 273 ),
244 274 default_value='multicolumn').tag(config=True)
245 275
246 276 highlight_matching_brackets = Bool(True,
247 277 help="Highlight matching brackets.",
248 278 ).tag(config=True)
249 279
250 280 extra_open_editor_shortcuts = Bool(False,
251 281 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
252 282 "This is in addition to the F2 binding, which is always enabled."
253 283 ).tag(config=True)
254 284
255 285 handle_return = Any(None,
256 286 help="Provide an alternative handler to be called when the user presses "
257 287 "Return. This is an advanced option intended for debugging, which "
258 288 "may be changed or removed in later releases."
259 289 ).tag(config=True)
260 290
261 291 enable_history_search = Bool(True,
262 292 help="Allows to enable/disable the prompt toolkit history search"
263 293 ).tag(config=True)
264 294
265 295 prompt_includes_vi_mode = Bool(True,
266 296 help="Display the current vi mode (when using vi editing mode)."
267 297 ).tag(config=True)
268 298
269 299 @observe('term_title')
270 300 def init_term_title(self, change=None):
271 301 # Enable or disable the terminal title.
272 302 if self.term_title:
273 303 toggle_set_term_title(True)
274 304 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
275 305 else:
276 306 toggle_set_term_title(False)
277 307
278 308 def restore_term_title(self):
279 309 if self.term_title:
280 310 restore_term_title()
281 311
282 312 def init_display_formatter(self):
283 313 super(TerminalInteractiveShell, self).init_display_formatter()
284 314 # terminal only supports plain text
285 315 self.display_formatter.active_types = ['text/plain']
286 316 # disable `_ipython_display_`
287 317 self.display_formatter.ipython_display_formatter.enabled = False
288 318
289 319 def init_prompt_toolkit_cli(self):
290 320 if self.simple_prompt:
291 321 # Fall back to plain non-interactive output for tests.
292 322 # This is very limited.
293 323 def prompt():
294 324 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
295 325 lines = [input(prompt_text)]
296 326 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
297 327 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
298 328 lines.append( input(prompt_continuation) )
299 329 return '\n'.join(lines)
300 330 self.prompt_for_code = prompt
301 331 return
302 332
303 333 # Set up keyboard shortcuts
304 334 key_bindings = create_ipython_shortcuts(self)
305 335
306 336 # Pre-populate history from IPython's history database
307 337 history = InMemoryHistory()
308 338 last_cell = u""
309 339 for __, ___, cell in self.history_manager.get_tail(self.history_load_length,
310 340 include_latest=True):
311 341 # Ignore blank lines and consecutive duplicates
312 342 cell = cell.rstrip()
313 343 if cell and (cell != last_cell):
314 344 history.append_string(cell)
315 345 last_cell = cell
316 346
317 347 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
318 348 self.style = DynamicStyle(lambda: self._style)
319 349
320 350 editing_mode = getattr(EditingMode, self.editing_mode.upper())
321 351
322 352 self.pt_loop = asyncio.new_event_loop()
323 353 self.pt_app = PromptSession(
324 354 editing_mode=editing_mode,
325 355 key_bindings=key_bindings,
326 356 history=history,
327 357 completer=IPythonPTCompleter(shell=self),
328 358 enable_history_search = self.enable_history_search,
329 359 style=self.style,
330 360 include_default_pygments_style=False,
331 361 mouse_support=self.mouse_support,
332 362 enable_open_in_editor=self.extra_open_editor_shortcuts,
333 363 color_depth=self.color_depth,
334 364 tempfile_suffix=".py",
335 365 **self._extra_prompt_options())
336 366
337 367 def _make_style_from_name_or_cls(self, name_or_cls):
338 368 """
339 369 Small wrapper that make an IPython compatible style from a style name
340 370
341 371 We need that to add style for prompt ... etc.
342 372 """
343 373 style_overrides = {}
344 374 if name_or_cls == 'legacy':
345 375 legacy = self.colors.lower()
346 376 if legacy == 'linux':
347 377 style_cls = get_style_by_name('monokai')
348 378 style_overrides = _style_overrides_linux
349 379 elif legacy == 'lightbg':
350 380 style_overrides = _style_overrides_light_bg
351 381 style_cls = get_style_by_name('pastie')
352 382 elif legacy == 'neutral':
353 383 # The default theme needs to be visible on both a dark background
354 384 # and a light background, because we can't tell what the terminal
355 385 # looks like. These tweaks to the default theme help with that.
356 386 style_cls = get_style_by_name('default')
357 387 style_overrides.update({
358 388 Token.Number: '#ansigreen',
359 389 Token.Operator: 'noinherit',
360 390 Token.String: '#ansiyellow',
361 391 Token.Name.Function: '#ansiblue',
362 392 Token.Name.Class: 'bold #ansiblue',
363 393 Token.Name.Namespace: 'bold #ansiblue',
364 394 Token.Name.Variable.Magic: '#ansiblue',
365 395 Token.Prompt: '#ansigreen',
366 396 Token.PromptNum: '#ansibrightgreen bold',
367 397 Token.OutPrompt: '#ansired',
368 398 Token.OutPromptNum: '#ansibrightred bold',
369 399 })
370 400
371 401 # Hack: Due to limited color support on the Windows console
372 402 # the prompt colors will be wrong without this
373 403 if os.name == 'nt':
374 404 style_overrides.update({
375 405 Token.Prompt: '#ansidarkgreen',
376 406 Token.PromptNum: '#ansigreen bold',
377 407 Token.OutPrompt: '#ansidarkred',
378 408 Token.OutPromptNum: '#ansired bold',
379 409 })
380 410 elif legacy =='nocolor':
381 411 style_cls=_NoStyle
382 412 style_overrides = {}
383 413 else :
384 414 raise ValueError('Got unknown colors: ', legacy)
385 415 else :
386 416 if isinstance(name_or_cls, str):
387 417 style_cls = get_style_by_name(name_or_cls)
388 418 else:
389 419 style_cls = name_or_cls
390 420 style_overrides = {
391 421 Token.Prompt: '#ansigreen',
392 422 Token.PromptNum: '#ansibrightgreen bold',
393 423 Token.OutPrompt: '#ansired',
394 424 Token.OutPromptNum: '#ansibrightred bold',
395 425 }
396 426 style_overrides.update(self.highlighting_style_overrides)
397 427 style = merge_styles([
398 428 style_from_pygments_cls(style_cls),
399 429 style_from_pygments_dict(style_overrides),
400 430 ])
401 431
402 432 return style
403 433
404 434 @property
405 435 def pt_complete_style(self):
406 436 return {
407 437 'multicolumn': CompleteStyle.MULTI_COLUMN,
408 438 'column': CompleteStyle.COLUMN,
409 439 'readlinelike': CompleteStyle.READLINE_LIKE,
410 440 }[self.display_completions]
411 441
412 442 @property
413 443 def color_depth(self):
414 444 return (ColorDepth.TRUE_COLOR if self.true_color else None)
415 445
416 446 def _extra_prompt_options(self):
417 447 """
418 448 Return the current layout option for the current Terminal InteractiveShell
419 449 """
420 450 def get_message():
421 451 return PygmentsTokens(self.prompts.in_prompt_tokens())
422 452
423 453 if self.editing_mode == 'emacs':
424 454 # with emacs mode the prompt is (usually) static, so we call only
425 455 # the function once. With VI mode it can toggle between [ins] and
426 456 # [nor] so we can't precompute.
427 457 # here I'm going to favor the default keybinding which almost
428 458 # everybody uses to decrease CPU usage.
429 459 # if we have issues with users with custom Prompts we can see how to
430 460 # work around this.
431 461 get_message = get_message()
432 462
433 463 options = {
434 464 'complete_in_thread': False,
435 465 'lexer':IPythonPTLexer(),
436 466 'reserve_space_for_menu':self.space_for_menu,
437 467 'message': get_message,
438 468 'prompt_continuation': (
439 469 lambda width, lineno, is_soft_wrap:
440 470 PygmentsTokens(self.prompts.continuation_prompt_tokens(width))),
441 471 'multiline': True,
442 472 'complete_style': self.pt_complete_style,
443 473
444 474 # Highlight matching brackets, but only when this setting is
445 475 # enabled, and only when the DEFAULT_BUFFER has the focus.
446 476 'input_processors': [ConditionalProcessor(
447 477 processor=HighlightMatchingBracketProcessor(chars='[](){}'),
448 478 filter=HasFocus(DEFAULT_BUFFER) & ~IsDone() &
449 479 Condition(lambda: self.highlight_matching_brackets))],
450 480 }
451 481 if not PTK3:
452 482 options['inputhook'] = self.inputhook
453 483
454 484 return options
455 485
456 486 def prompt_for_code(self):
457 487 if self.rl_next_input:
458 488 default = self.rl_next_input
459 489 self.rl_next_input = None
460 490 else:
461 491 default = ''
462 492
463 493 # In order to make sure that asyncio code written in the
464 494 # interactive shell doesn't interfere with the prompt, we run the
465 495 # prompt in a different event loop.
466 496 # If we don't do this, people could spawn coroutine with a
467 497 # while/true inside which will freeze the prompt.
468 498
469 499 try:
470 500 old_loop = asyncio.get_event_loop()
471 501 except RuntimeError:
472 502 # This happens when the user used `asyncio.run()`.
473 503 old_loop = None
474 504
475 505 asyncio.set_event_loop(self.pt_loop)
476 506 try:
477 507 with patch_stdout(raw=True):
478 508 text = self.pt_app.prompt(
479 509 default=default,
480 510 **self._extra_prompt_options())
481 511 finally:
482 512 # Restore the original event loop.
483 513 asyncio.set_event_loop(old_loop)
484 514
485 515 return text
486 516
487 517 def enable_win_unicode_console(self):
488 518 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
489 519 # console by default, so WUC shouldn't be needed.
490 520 from warnings import warn
491 521 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
492 522 DeprecationWarning,
493 523 stacklevel=2)
494 524
495 525 def init_io(self):
496 526 if sys.platform not in {'win32', 'cli'}:
497 527 return
498 528
499 529 import colorama
500 530 colorama.init()
501 531
502 532 # For some reason we make these wrappers around stdout/stderr.
503 533 # For now, we need to reset them so all output gets coloured.
504 534 # https://github.com/ipython/ipython/issues/8669
505 535 # io.std* are deprecated, but don't show our own deprecation warnings
506 536 # during initialization of the deprecated API.
507 537 with warnings.catch_warnings():
508 538 warnings.simplefilter('ignore', DeprecationWarning)
509 539 io.stdout = io.IOStream(sys.stdout)
510 540 io.stderr = io.IOStream(sys.stderr)
511 541
512 542 def init_magics(self):
513 543 super(TerminalInteractiveShell, self).init_magics()
514 544 self.register_magics(TerminalMagics)
515 545
516 546 def init_alias(self):
517 547 # The parent class defines aliases that can be safely used with any
518 548 # frontend.
519 549 super(TerminalInteractiveShell, self).init_alias()
520 550
521 551 # Now define aliases that only make sense on the terminal, because they
522 552 # need direct access to the console in a way that we can't emulate in
523 553 # GUI or web frontend
524 554 if os.name == 'posix':
525 555 for cmd in ('clear', 'more', 'less', 'man'):
526 556 self.alias_manager.soft_define_alias(cmd, cmd)
527 557
528 558
529 559 def __init__(self, *args, **kwargs):
530 560 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
531 561 self.init_prompt_toolkit_cli()
532 562 self.init_term_title()
533 563 self.keep_running = True
534 564
535 565 self.debugger_history = InMemoryHistory()
536 566
537 567 def ask_exit(self):
538 568 self.keep_running = False
539 569
540 570 rl_next_input = None
541 571
542 572 def interact(self, display_banner=DISPLAY_BANNER_DEPRECATED):
543 573
544 574 if display_banner is not DISPLAY_BANNER_DEPRECATED:
545 575 warn('interact `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
546 576
547 577 self.keep_running = True
548 578 while self.keep_running:
549 579 print(self.separate_in, end='')
550 580
551 581 try:
552 582 code = self.prompt_for_code()
553 583 except EOFError:
554 584 if (not self.confirm_exit) \
555 585 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
556 586 self.ask_exit()
557 587
558 588 else:
559 589 if code:
560 590 self.run_cell(code, store_history=True)
561 591
562 592 def mainloop(self, display_banner=DISPLAY_BANNER_DEPRECATED):
563 593 # An extra layer of protection in case someone mashing Ctrl-C breaks
564 594 # out of our internal code.
565 595 if display_banner is not DISPLAY_BANNER_DEPRECATED:
566 596 warn('mainloop `display_banner` argument is deprecated since IPython 5.0. Call `show_banner()` if needed.', DeprecationWarning, stacklevel=2)
567 597 while True:
568 598 try:
569 599 self.interact()
570 600 break
571 601 except KeyboardInterrupt as e:
572 602 print("\n%s escaped interact()\n" % type(e).__name__)
573 603 finally:
574 604 # An interrupt during the eventloop will mess up the
575 605 # internal state of the prompt_toolkit library.
576 606 # Stopping the eventloop fixes this, see
577 607 # https://github.com/ipython/ipython/pull/9867
578 608 if hasattr(self, '_eventloop'):
579 609 self._eventloop.stop()
580 610
581 611 self.restore_term_title()
582 612
583 613
584 614 _inputhook = None
585 615 def inputhook(self, context):
586 616 if self._inputhook is not None:
587 617 self._inputhook(context)
588 618
589 619 active_eventloop = None
590 620 def enable_gui(self, gui=None):
591 621 if gui and (gui != 'inline') :
592 622 self.active_eventloop, self._inputhook =\
593 623 get_inputhook_name_and_func(gui)
594 624 else:
595 625 self.active_eventloop = self._inputhook = None
596 626
597 627 # For prompt_toolkit 3.0. We have to create an asyncio event loop with
598 628 # this inputhook.
599 629 if PTK3:
600 630 import asyncio
601 631 from prompt_toolkit.eventloop import new_eventloop_with_inputhook
602 632
603 633 if gui == 'asyncio':
604 634 # When we integrate the asyncio event loop, run the UI in the
605 635 # same event loop as the rest of the code. don't use an actual
606 636 # input hook. (Asyncio is not made for nesting event loops.)
607 637 self.pt_loop = asyncio.get_event_loop()
608 638
609 639 elif self._inputhook:
610 640 # If an inputhook was set, create a new asyncio event loop with
611 641 # this inputhook for the prompt.
612 642 self.pt_loop = new_eventloop_with_inputhook(self._inputhook)
613 643 else:
614 644 # When there's no inputhook, run the prompt in a separate
615 645 # asyncio event loop.
616 646 self.pt_loop = asyncio.new_event_loop()
617 647
618 648 # Run !system commands directly, not through pipes, so terminal programs
619 649 # work correctly.
620 650 system = InteractiveShell.system_raw
621 651
622 652 def auto_rewrite_input(self, cmd):
623 653 """Overridden from the parent class to use fancy rewriting prompt"""
624 654 if not self.show_rewritten_input:
625 655 return
626 656
627 657 tokens = self.prompts.rewrite_prompt_tokens()
628 658 if self.pt_app:
629 659 print_formatted_text(PygmentsTokens(tokens), end='',
630 660 style=self.pt_app.app.style)
631 661 print(cmd)
632 662 else:
633 663 prompt = ''.join(s for t, s in tokens)
634 664 print(prompt, cmd, sep='')
635 665
636 666 _prompts_before = None
637 667 def switch_doctest_mode(self, mode):
638 668 """Switch prompts to classic for %doctest_mode"""
639 669 if mode:
640 670 self._prompts_before = self.prompts
641 671 self.prompts = ClassicPrompts(self)
642 672 elif self._prompts_before:
643 673 self.prompts = self._prompts_before
644 674 self._prompts_before = None
645 675 # self._update_layout()
646 676
647 677
648 678 InteractiveShellABC.register(TerminalInteractiveShell)
649 679
650 680 if __name__ == '__main__':
651 681 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))
General Comments 0
You need to be logged in to leave comments. Login now