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