##// END OF EJS Templates
Expose `auto_suggest.resume_hinting`, fix resume on backspace
krassowski -
Show More
@@ -1,992 +1,993 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 from warnings import warn
7 7 from typing import Union as UnionType
8 8
9 9 from IPython.core.async_helpers import get_asyncio_loop
10 10 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
11 11 from IPython.utils.py3compat import input
12 12 from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title
13 13 from IPython.utils.process import abbrev_cwd
14 14 from traitlets import (
15 15 Bool,
16 16 Unicode,
17 17 Dict,
18 18 Integer,
19 19 List,
20 20 observe,
21 21 Instance,
22 22 Type,
23 23 default,
24 24 Enum,
25 25 Union,
26 26 Any,
27 27 validate,
28 28 Float,
29 29 )
30 30
31 31 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
32 32 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
33 33 from prompt_toolkit.filters import HasFocus, Condition, IsDone
34 34 from prompt_toolkit.formatted_text import PygmentsTokens
35 35 from prompt_toolkit.history import History
36 36 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
37 37 from prompt_toolkit.output import ColorDepth
38 38 from prompt_toolkit.patch_stdout import patch_stdout
39 39 from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
40 40 from prompt_toolkit.styles import DynamicStyle, merge_styles
41 41 from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
42 42 from prompt_toolkit import __version__ as ptk_version
43 43
44 44 from pygments.styles import get_style_by_name
45 45 from pygments.style import Style
46 46 from pygments.token import Token
47 47
48 48 from .debugger import TerminalPdb, Pdb
49 49 from .magics import TerminalMagics
50 50 from .pt_inputhooks import get_inputhook_name_and_func
51 51 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
52 52 from .ptutils import IPythonPTCompleter, IPythonPTLexer
53 53 from .shortcuts import (
54 KEY_BINDINGS,
54 55 create_ipython_shortcuts,
55 56 create_identifier,
56 57 RuntimeBinding,
57 58 add_binding,
58 59 )
59 60 from .shortcuts.filters import KEYBINDING_FILTERS, filter_from_string
60 61 from .shortcuts.auto_suggest import (
61 62 NavigableAutoSuggestFromHistory,
62 63 AppendAutoSuggestionInAnyLine,
63 64 )
64 65
65 66 PTK3 = ptk_version.startswith('3.')
66 67
67 68
68 69 class _NoStyle(Style): pass
69 70
70 71
71 72
72 73 _style_overrides_light_bg = {
73 74 Token.Prompt: '#ansibrightblue',
74 75 Token.PromptNum: '#ansiblue bold',
75 76 Token.OutPrompt: '#ansibrightred',
76 77 Token.OutPromptNum: '#ansired bold',
77 78 }
78 79
79 80 _style_overrides_linux = {
80 81 Token.Prompt: '#ansibrightgreen',
81 82 Token.PromptNum: '#ansigreen bold',
82 83 Token.OutPrompt: '#ansibrightred',
83 84 Token.OutPromptNum: '#ansired bold',
84 85 }
85 86
86 87 def get_default_editor():
87 88 try:
88 89 return os.environ['EDITOR']
89 90 except KeyError:
90 91 pass
91 92 except UnicodeError:
92 93 warn("$EDITOR environment variable is not pure ASCII. Using platform "
93 94 "default editor.")
94 95
95 96 if os.name == 'posix':
96 97 return 'vi' # the only one guaranteed to be there!
97 98 else:
98 99 return 'notepad' # same in Windows!
99 100
100 101 # conservatively check for tty
101 102 # overridden streams can result in things like:
102 103 # - sys.stdin = None
103 104 # - no isatty method
104 105 for _name in ('stdin', 'stdout', 'stderr'):
105 106 _stream = getattr(sys, _name)
106 107 try:
107 108 if not _stream or not hasattr(_stream, "isatty") or not _stream.isatty():
108 109 _is_tty = False
109 110 break
110 111 except ValueError:
111 112 # stream is closed
112 113 _is_tty = False
113 114 break
114 115 else:
115 116 _is_tty = True
116 117
117 118
118 119 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
119 120
120 121 def black_reformat_handler(text_before_cursor):
121 122 """
122 123 We do not need to protect against error,
123 124 this is taken care at a higher level where any reformat error is ignored.
124 125 Indeed we may call reformatting on incomplete code.
125 126 """
126 127 import black
127 128
128 129 formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
129 130 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
130 131 formatted_text = formatted_text[:-1]
131 132 return formatted_text
132 133
133 134
134 135 def yapf_reformat_handler(text_before_cursor):
135 136 from yapf.yapflib import file_resources
136 137 from yapf.yapflib import yapf_api
137 138
138 139 style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
139 140 formatted_text, was_formatted = yapf_api.FormatCode(
140 141 text_before_cursor, style_config=style_config
141 142 )
142 143 if was_formatted:
143 144 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
144 145 formatted_text = formatted_text[:-1]
145 146 return formatted_text
146 147 else:
147 148 return text_before_cursor
148 149
149 150
150 151 class PtkHistoryAdapter(History):
151 152 """
152 153 Prompt toolkit has it's own way of handling history, Where it assumes it can
153 154 Push/pull from history.
154 155
155 156 """
156 157
157 158 def __init__(self, shell):
158 159 super().__init__()
159 160 self.shell = shell
160 161 self._refresh()
161 162
162 163 def append_string(self, string):
163 164 # we rely on sql for that.
164 165 self._loaded = False
165 166 self._refresh()
166 167
167 168 def _refresh(self):
168 169 if not self._loaded:
169 170 self._loaded_strings = list(self.load_history_strings())
170 171
171 172 def load_history_strings(self):
172 173 last_cell = ""
173 174 res = []
174 175 for __, ___, cell in self.shell.history_manager.get_tail(
175 176 self.shell.history_load_length, include_latest=True
176 177 ):
177 178 # Ignore blank lines and consecutive duplicates
178 179 cell = cell.rstrip()
179 180 if cell and (cell != last_cell):
180 181 res.append(cell)
181 182 last_cell = cell
182 183 yield from res[::-1]
183 184
184 185 def store_string(self, string: str) -> None:
185 186 pass
186 187
187 188 class TerminalInteractiveShell(InteractiveShell):
188 189 mime_renderers = Dict().tag(config=True)
189 190
190 191 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
191 192 'to reserve for the tab completion menu, '
192 193 'search history, ...etc, the height of '
193 194 'these menus will at most this value. '
194 195 'Increase it is you prefer long and skinny '
195 196 'menus, decrease for short and wide.'
196 197 ).tag(config=True)
197 198
198 199 pt_app: UnionType[PromptSession, None] = None
199 200 auto_suggest: UnionType[
200 201 AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None
201 202 ] = None
202 203 debugger_history = None
203 204
204 205 debugger_history_file = Unicode(
205 206 "~/.pdbhistory", help="File in which to store and read history"
206 207 ).tag(config=True)
207 208
208 209 simple_prompt = Bool(_use_simple_prompt,
209 210 help="""Use `raw_input` for the REPL, without completion and prompt colors.
210 211
211 212 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
212 213 IPython own testing machinery, and emacs inferior-shell integration through elpy.
213 214
214 215 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
215 216 environment variable is set, or the current terminal is not a tty."""
216 217 ).tag(config=True)
217 218
218 219 @property
219 220 def debugger_cls(self):
220 221 return Pdb if self.simple_prompt else TerminalPdb
221 222
222 223 confirm_exit = Bool(True,
223 224 help="""
224 225 Set to confirm when you try to exit IPython with an EOF (Control-D
225 226 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
226 227 you can force a direct exit without any confirmation.""",
227 228 ).tag(config=True)
228 229
229 230 editing_mode = Unicode('emacs',
230 231 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
231 232 ).tag(config=True)
232 233
233 234 emacs_bindings_in_vi_insert_mode = Bool(
234 235 True,
235 236 help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.",
236 237 ).tag(config=True)
237 238
238 239 modal_cursor = Bool(
239 240 True,
240 241 help="""
241 242 Cursor shape changes depending on vi mode: beam in vi insert mode,
242 243 block in nav mode, underscore in replace mode.""",
243 244 ).tag(config=True)
244 245
245 246 ttimeoutlen = Float(
246 247 0.01,
247 248 help="""The time in milliseconds that is waited for a key code
248 249 to complete.""",
249 250 ).tag(config=True)
250 251
251 252 timeoutlen = Float(
252 253 0.5,
253 254 help="""The time in milliseconds that is waited for a mapped key
254 255 sequence to complete.""",
255 256 ).tag(config=True)
256 257
257 258 autoformatter = Unicode(
258 259 None,
259 260 help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`",
260 261 allow_none=True
261 262 ).tag(config=True)
262 263
263 264 auto_match = Bool(
264 265 False,
265 266 help="""
266 267 Automatically add/delete closing bracket or quote when opening bracket or quote is entered/deleted.
267 268 Brackets: (), [], {}
268 269 Quotes: '', \"\"
269 270 """,
270 271 ).tag(config=True)
271 272
272 273 mouse_support = Bool(False,
273 274 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
274 275 ).tag(config=True)
275 276
276 277 # We don't load the list of styles for the help string, because loading
277 278 # Pygments plugins takes time and can cause unexpected errors.
278 279 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
279 280 help="""The name or class of a Pygments style to use for syntax
280 281 highlighting. To see available styles, run `pygmentize -L styles`."""
281 282 ).tag(config=True)
282 283
283 284 @validate('editing_mode')
284 285 def _validate_editing_mode(self, proposal):
285 286 if proposal['value'].lower() == 'vim':
286 287 proposal['value']= 'vi'
287 288 elif proposal['value'].lower() == 'default':
288 289 proposal['value']= 'emacs'
289 290
290 291 if hasattr(EditingMode, proposal['value'].upper()):
291 292 return proposal['value'].lower()
292 293
293 294 return self.editing_mode
294 295
295 296
296 297 @observe('editing_mode')
297 298 def _editing_mode(self, change):
298 299 if self.pt_app:
299 300 self.pt_app.editing_mode = getattr(EditingMode, change.new.upper())
300 301
301 302 def _set_formatter(self, formatter):
302 303 if formatter is None:
303 304 self.reformat_handler = lambda x:x
304 305 elif formatter == 'black':
305 306 self.reformat_handler = black_reformat_handler
306 307 elif formatter == "yapf":
307 308 self.reformat_handler = yapf_reformat_handler
308 309 else:
309 310 raise ValueError
310 311
311 312 @observe("autoformatter")
312 313 def _autoformatter_changed(self, change):
313 314 formatter = change.new
314 315 self._set_formatter(formatter)
315 316
316 317 @observe('highlighting_style')
317 318 @observe('colors')
318 319 def _highlighting_style_changed(self, change):
319 320 self.refresh_style()
320 321
321 322 def refresh_style(self):
322 323 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
323 324
324 325
325 326 highlighting_style_overrides = Dict(
326 327 help="Override highlighting format for specific tokens"
327 328 ).tag(config=True)
328 329
329 330 true_color = Bool(False,
330 331 help="""Use 24bit colors instead of 256 colors in prompt highlighting.
331 332 If your terminal supports true color, the following command should
332 333 print ``TRUECOLOR`` in orange::
333 334
334 335 printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"
335 336 """,
336 337 ).tag(config=True)
337 338
338 339 editor = Unicode(get_default_editor(),
339 340 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
340 341 ).tag(config=True)
341 342
342 343 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
343 344
344 345 prompts = Instance(Prompts)
345 346
346 347 @default('prompts')
347 348 def _prompts_default(self):
348 349 return self.prompts_class(self)
349 350
350 351 # @observe('prompts')
351 352 # def _(self, change):
352 353 # self._update_layout()
353 354
354 355 @default('displayhook_class')
355 356 def _displayhook_class_default(self):
356 357 return RichPromptDisplayHook
357 358
358 359 term_title = Bool(True,
359 360 help="Automatically set the terminal title"
360 361 ).tag(config=True)
361 362
362 363 term_title_format = Unicode("IPython: {cwd}",
363 364 help="Customize the terminal title format. This is a python format string. " +
364 365 "Available substitutions are: {cwd}."
365 366 ).tag(config=True)
366 367
367 368 display_completions = Enum(('column', 'multicolumn','readlinelike'),
368 369 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
369 370 "'readlinelike'. These options are for `prompt_toolkit`, see "
370 371 "`prompt_toolkit` documentation for more information."
371 372 ),
372 373 default_value='multicolumn').tag(config=True)
373 374
374 375 highlight_matching_brackets = Bool(True,
375 376 help="Highlight matching brackets.",
376 377 ).tag(config=True)
377 378
378 379 extra_open_editor_shortcuts = Bool(False,
379 380 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
380 381 "This is in addition to the F2 binding, which is always enabled."
381 382 ).tag(config=True)
382 383
383 384 handle_return = Any(None,
384 385 help="Provide an alternative handler to be called when the user presses "
385 386 "Return. This is an advanced option intended for debugging, which "
386 387 "may be changed or removed in later releases."
387 388 ).tag(config=True)
388 389
389 390 enable_history_search = Bool(True,
390 391 help="Allows to enable/disable the prompt toolkit history search"
391 392 ).tag(config=True)
392 393
393 394 autosuggestions_provider = Unicode(
394 395 "NavigableAutoSuggestFromHistory",
395 396 help="Specifies from which source automatic suggestions are provided. "
396 397 "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and "
397 398 ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, "
398 399 " or ``None`` to disable automatic suggestions. "
399 400 "Default is `'NavigableAutoSuggestFromHistory`'.",
400 401 allow_none=True,
401 402 ).tag(config=True)
402 403
403 404 def _set_autosuggestions(self, provider):
404 405 # disconnect old handler
405 406 if self.auto_suggest and isinstance(
406 407 self.auto_suggest, NavigableAutoSuggestFromHistory
407 408 ):
408 409 self.auto_suggest.disconnect()
409 410 if provider is None:
410 411 self.auto_suggest = None
411 412 elif provider == "AutoSuggestFromHistory":
412 413 self.auto_suggest = AutoSuggestFromHistory()
413 414 elif provider == "NavigableAutoSuggestFromHistory":
414 415 self.auto_suggest = NavigableAutoSuggestFromHistory()
415 416 else:
416 417 raise ValueError("No valid provider.")
417 418 if self.pt_app:
418 419 self.pt_app.auto_suggest = self.auto_suggest
419 420
420 421 @observe("autosuggestions_provider")
421 422 def _autosuggestions_provider_changed(self, change):
422 423 provider = change.new
423 424 self._set_autosuggestions(provider)
424 425
425 426 shortcuts = List(
426 427 trait=Dict(
427 428 key_trait=Enum(
428 429 [
429 430 "command",
430 431 "match_keys",
431 432 "match_filter",
432 433 "new_keys",
433 434 "new_filter",
434 435 "create",
435 436 ]
436 437 ),
437 438 per_key_traits={
438 439 "command": Unicode(),
439 440 "match_keys": List(Unicode()),
440 441 "match_filter": Unicode(),
441 442 "new_keys": List(Unicode()),
442 443 "new_filter": Unicode(),
443 444 "create": Bool(False),
444 445 },
445 446 ),
446 447 help="""Add, disable or modifying shortcuts.
447 448
448 449 Each entry on the list should be a dictionary with ``command`` key
449 450 identifying the target function executed by the shortcut and at least
450 451 one of the following:
451 452
452 453 - ``match_keys``: list of keys used to match an existing shortcut,
453 454 - ``match_filter``: shortcut filter used to match an existing shortcut,
454 455 - ``new_keys``: list of keys to set,
455 456 - ``new_filter``: a new shortcut filter to set
456 457
457 458 The filters have to be composed of pre-defined verbs and joined by one
458 459 of the following conjunctions: ``&`` (and), ``|`` (or), ``~`` (not).
459 460 The pre-defined verbs are:
460 461
461 462 {}
462 463
463 464
464 465 To disable a shortcut set ``new_keys`` to an empty list.
465 466 To add a shortcut add key ``create`` with value ``True``.
466 467
467 468 When modifying/disabling shortcuts, ``match_keys``/``match_filter`` can
468 469 be omitted if the provided specification uniquely identifies a shortcut
469 470 to be modified/disabled. When modifying a shortcut ``new_filter`` or
470 471 ``new_keys`` can be omitted which will result in reuse of the existing
471 472 filter/keys.
472 473
473 474 Only shortcuts defined in IPython (and not default prompt-toolkit
474 475 shortcuts) can be modified or disabled. The full list of shortcuts,
475 476 command identifiers and filters is available under
476 477 :ref:`terminal-shortcuts-list`.
477 478 """.format(
478 479 "\n ".join([f"- `{k}`" for k in KEYBINDING_FILTERS])
479 480 ),
480 481 ).tag(config=True)
481 482
482 483 @observe("shortcuts")
483 484 def _shortcuts_changed(self, change):
484 485 if self.pt_app:
485 486 self.pt_app.key_bindings = self._merge_shortcuts(user_shortcuts=change.new)
486 487
487 488 def _merge_shortcuts(self, user_shortcuts):
488 489 # rebuild the bindings list from scratch
489 490 key_bindings = create_ipython_shortcuts(self)
490 491
491 492 # for now we only allow adding shortcuts for commands which are already
492 493 # registered; this is a security precaution.
493 494 known_commands = {
494 create_identifier(binding.handler): binding.handler
495 for binding in key_bindings.bindings
495 create_identifier(binding.command): binding.command
496 for binding in KEY_BINDINGS
496 497 }
497 498 shortcuts_to_skip = []
498 499 shortcuts_to_add = []
499 500
500 501 for shortcut in user_shortcuts:
501 502 command_id = shortcut["command"]
502 503 if command_id not in known_commands:
503 504 allowed_commands = "\n - ".join(known_commands)
504 505 raise ValueError(
505 506 f"{command_id} is not a known shortcut command."
506 507 f" Allowed commands are: \n - {allowed_commands}"
507 508 )
508 509 old_keys = shortcut.get("match_keys", None)
509 510 old_filter = (
510 511 filter_from_string(shortcut["match_filter"])
511 512 if "match_filter" in shortcut
512 513 else None
513 514 )
514 515 matching = [
515 516 binding
516 for binding in key_bindings.bindings
517 for binding in KEY_BINDINGS
517 518 if (
518 519 (old_filter is None or binding.filter == old_filter)
519 520 and (old_keys is None or [k for k in binding.keys] == old_keys)
520 and create_identifier(binding.handler) == command_id
521 and create_identifier(binding.command) == command_id
521 522 )
522 523 ]
523 524
524 525 new_keys = shortcut.get("new_keys", None)
525 526 new_filter = shortcut.get("new_filter", None)
526 527
527 528 command = known_commands[command_id]
528 529
529 530 creating_new = shortcut.get("create", False)
530 531 modifying_existing = not creating_new and (
531 532 new_keys is not None or new_filter
532 533 )
533 534
534 535 if creating_new and new_keys == []:
535 536 raise ValueError("Cannot add a shortcut without keys")
536 537
537 538 if modifying_existing:
538 539 specification = {
539 540 key: shortcut[key]
540 541 for key in ["command", "filter"]
541 542 if key in shortcut
542 543 }
543 544 if len(matching) == 0:
544 545 raise ValueError(
545 f"No shortcuts matching {specification} found in {key_bindings.bindings}"
546 f"No shortcuts matching {specification} found in {KEY_BINDINGS}"
546 547 )
547 548 elif len(matching) > 1:
548 549 raise ValueError(
549 550 f"Multiple shortcuts matching {specification} found,"
550 551 f" please add keys/filter to select one of: {matching}"
551 552 )
552 553
553 554 matched = matching[0]
554 555 old_filter = matched.filter
555 556 old_keys = list(matched.keys)
556 557 shortcuts_to_skip.append(
557 558 RuntimeBinding(
558 559 command,
559 560 keys=old_keys,
560 561 filter=old_filter,
561 562 )
562 563 )
563 564
564 565 if new_keys != []:
565 566 shortcuts_to_add.append(
566 567 RuntimeBinding(
567 568 command,
568 569 keys=new_keys or old_keys,
569 570 filter=filter_from_string(new_filter)
570 571 if new_filter is not None
571 572 else (
572 573 old_filter
573 574 if old_filter is not None
574 575 else filter_from_string("always")
575 576 ),
576 577 )
577 578 )
578 579
579 580 # rebuild the bindings list from scratch
580 581 key_bindings = create_ipython_shortcuts(self, skip=shortcuts_to_skip)
581 582 for binding in shortcuts_to_add:
582 583 add_binding(key_bindings, binding)
583 584
584 585 return key_bindings
585 586
586 587 prompt_includes_vi_mode = Bool(True,
587 588 help="Display the current vi mode (when using vi editing mode)."
588 589 ).tag(config=True)
589 590
590 591 @observe('term_title')
591 592 def init_term_title(self, change=None):
592 593 # Enable or disable the terminal title.
593 594 if self.term_title and _is_tty:
594 595 toggle_set_term_title(True)
595 596 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
596 597 else:
597 598 toggle_set_term_title(False)
598 599
599 600 def restore_term_title(self):
600 601 if self.term_title and _is_tty:
601 602 restore_term_title()
602 603
603 604 def init_display_formatter(self):
604 605 super(TerminalInteractiveShell, self).init_display_formatter()
605 606 # terminal only supports plain text
606 607 self.display_formatter.active_types = ["text/plain"]
607 608
608 609 def init_prompt_toolkit_cli(self):
609 610 if self.simple_prompt:
610 611 # Fall back to plain non-interactive output for tests.
611 612 # This is very limited.
612 613 def prompt():
613 614 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
614 615 lines = [input(prompt_text)]
615 616 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
616 617 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
617 618 lines.append( input(prompt_continuation) )
618 619 return '\n'.join(lines)
619 620 self.prompt_for_code = prompt
620 621 return
621 622
622 623 # Set up keyboard shortcuts
623 624 key_bindings = self._merge_shortcuts(user_shortcuts=self.shortcuts)
624 625
625 626 # Pre-populate history from IPython's history database
626 627 history = PtkHistoryAdapter(self)
627 628
628 629 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
629 630 self.style = DynamicStyle(lambda: self._style)
630 631
631 632 editing_mode = getattr(EditingMode, self.editing_mode.upper())
632 633
633 634 self.pt_loop = asyncio.new_event_loop()
634 635 self.pt_app = PromptSession(
635 636 auto_suggest=self.auto_suggest,
636 637 editing_mode=editing_mode,
637 638 key_bindings=key_bindings,
638 639 history=history,
639 640 completer=IPythonPTCompleter(shell=self),
640 641 enable_history_search=self.enable_history_search,
641 642 style=self.style,
642 643 include_default_pygments_style=False,
643 644 mouse_support=self.mouse_support,
644 645 enable_open_in_editor=self.extra_open_editor_shortcuts,
645 646 color_depth=self.color_depth,
646 647 tempfile_suffix=".py",
647 648 **self._extra_prompt_options(),
648 649 )
649 650 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
650 651 self.auto_suggest.connect(self.pt_app)
651 652
652 653 def _make_style_from_name_or_cls(self, name_or_cls):
653 654 """
654 655 Small wrapper that make an IPython compatible style from a style name
655 656
656 657 We need that to add style for prompt ... etc.
657 658 """
658 659 style_overrides = {}
659 660 if name_or_cls == 'legacy':
660 661 legacy = self.colors.lower()
661 662 if legacy == 'linux':
662 663 style_cls = get_style_by_name('monokai')
663 664 style_overrides = _style_overrides_linux
664 665 elif legacy == 'lightbg':
665 666 style_overrides = _style_overrides_light_bg
666 667 style_cls = get_style_by_name('pastie')
667 668 elif legacy == 'neutral':
668 669 # The default theme needs to be visible on both a dark background
669 670 # and a light background, because we can't tell what the terminal
670 671 # looks like. These tweaks to the default theme help with that.
671 672 style_cls = get_style_by_name('default')
672 673 style_overrides.update({
673 674 Token.Number: '#ansigreen',
674 675 Token.Operator: 'noinherit',
675 676 Token.String: '#ansiyellow',
676 677 Token.Name.Function: '#ansiblue',
677 678 Token.Name.Class: 'bold #ansiblue',
678 679 Token.Name.Namespace: 'bold #ansiblue',
679 680 Token.Name.Variable.Magic: '#ansiblue',
680 681 Token.Prompt: '#ansigreen',
681 682 Token.PromptNum: '#ansibrightgreen bold',
682 683 Token.OutPrompt: '#ansired',
683 684 Token.OutPromptNum: '#ansibrightred bold',
684 685 })
685 686
686 687 # Hack: Due to limited color support on the Windows console
687 688 # the prompt colors will be wrong without this
688 689 if os.name == 'nt':
689 690 style_overrides.update({
690 691 Token.Prompt: '#ansidarkgreen',
691 692 Token.PromptNum: '#ansigreen bold',
692 693 Token.OutPrompt: '#ansidarkred',
693 694 Token.OutPromptNum: '#ansired bold',
694 695 })
695 696 elif legacy =='nocolor':
696 697 style_cls=_NoStyle
697 698 style_overrides = {}
698 699 else :
699 700 raise ValueError('Got unknown colors: ', legacy)
700 701 else :
701 702 if isinstance(name_or_cls, str):
702 703 style_cls = get_style_by_name(name_or_cls)
703 704 else:
704 705 style_cls = name_or_cls
705 706 style_overrides = {
706 707 Token.Prompt: '#ansigreen',
707 708 Token.PromptNum: '#ansibrightgreen bold',
708 709 Token.OutPrompt: '#ansired',
709 710 Token.OutPromptNum: '#ansibrightred bold',
710 711 }
711 712 style_overrides.update(self.highlighting_style_overrides)
712 713 style = merge_styles([
713 714 style_from_pygments_cls(style_cls),
714 715 style_from_pygments_dict(style_overrides),
715 716 ])
716 717
717 718 return style
718 719
719 720 @property
720 721 def pt_complete_style(self):
721 722 return {
722 723 'multicolumn': CompleteStyle.MULTI_COLUMN,
723 724 'column': CompleteStyle.COLUMN,
724 725 'readlinelike': CompleteStyle.READLINE_LIKE,
725 726 }[self.display_completions]
726 727
727 728 @property
728 729 def color_depth(self):
729 730 return (ColorDepth.TRUE_COLOR if self.true_color else None)
730 731
731 732 def _extra_prompt_options(self):
732 733 """
733 734 Return the current layout option for the current Terminal InteractiveShell
734 735 """
735 736 def get_message():
736 737 return PygmentsTokens(self.prompts.in_prompt_tokens())
737 738
738 739 if self.editing_mode == 'emacs':
739 740 # with emacs mode the prompt is (usually) static, so we call only
740 741 # the function once. With VI mode it can toggle between [ins] and
741 742 # [nor] so we can't precompute.
742 743 # here I'm going to favor the default keybinding which almost
743 744 # everybody uses to decrease CPU usage.
744 745 # if we have issues with users with custom Prompts we can see how to
745 746 # work around this.
746 747 get_message = get_message()
747 748
748 749 options = {
749 750 "complete_in_thread": False,
750 751 "lexer": IPythonPTLexer(),
751 752 "reserve_space_for_menu": self.space_for_menu,
752 753 "message": get_message,
753 754 "prompt_continuation": (
754 755 lambda width, lineno, is_soft_wrap: PygmentsTokens(
755 756 self.prompts.continuation_prompt_tokens(width)
756 757 )
757 758 ),
758 759 "multiline": True,
759 760 "complete_style": self.pt_complete_style,
760 761 "input_processors": [
761 762 # Highlight matching brackets, but only when this setting is
762 763 # enabled, and only when the DEFAULT_BUFFER has the focus.
763 764 ConditionalProcessor(
764 765 processor=HighlightMatchingBracketProcessor(chars="[](){}"),
765 766 filter=HasFocus(DEFAULT_BUFFER)
766 767 & ~IsDone()
767 768 & Condition(lambda: self.highlight_matching_brackets),
768 769 ),
769 770 # Show auto-suggestion in lines other than the last line.
770 771 ConditionalProcessor(
771 772 processor=AppendAutoSuggestionInAnyLine(),
772 773 filter=HasFocus(DEFAULT_BUFFER)
773 774 & ~IsDone()
774 775 & Condition(
775 776 lambda: isinstance(
776 777 self.auto_suggest, NavigableAutoSuggestFromHistory
777 778 )
778 779 ),
779 780 ),
780 781 ],
781 782 }
782 783 if not PTK3:
783 784 options['inputhook'] = self.inputhook
784 785
785 786 return options
786 787
787 788 def prompt_for_code(self):
788 789 if self.rl_next_input:
789 790 default = self.rl_next_input
790 791 self.rl_next_input = None
791 792 else:
792 793 default = ''
793 794
794 795 # In order to make sure that asyncio code written in the
795 796 # interactive shell doesn't interfere with the prompt, we run the
796 797 # prompt in a different event loop.
797 798 # If we don't do this, people could spawn coroutine with a
798 799 # while/true inside which will freeze the prompt.
799 800
800 801 policy = asyncio.get_event_loop_policy()
801 802 old_loop = get_asyncio_loop()
802 803
803 804 # FIXME: prompt_toolkit is using the deprecated `asyncio.get_event_loop`
804 805 # to get the current event loop.
805 806 # This will probably be replaced by an attribute or input argument,
806 807 # at which point we can stop calling the soon-to-be-deprecated `set_event_loop` here.
807 808 if old_loop is not self.pt_loop:
808 809 policy.set_event_loop(self.pt_loop)
809 810 try:
810 811 with patch_stdout(raw=True):
811 812 text = self.pt_app.prompt(
812 813 default=default,
813 814 **self._extra_prompt_options())
814 815 finally:
815 816 # Restore the original event loop.
816 817 if old_loop is not None and old_loop is not self.pt_loop:
817 818 policy.set_event_loop(old_loop)
818 819
819 820 return text
820 821
821 822 def enable_win_unicode_console(self):
822 823 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
823 824 # console by default, so WUC shouldn't be needed.
824 825 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
825 826 DeprecationWarning,
826 827 stacklevel=2)
827 828
828 829 def init_io(self):
829 830 if sys.platform not in {'win32', 'cli'}:
830 831 return
831 832
832 833 import colorama
833 834 colorama.init()
834 835
835 836 def init_magics(self):
836 837 super(TerminalInteractiveShell, self).init_magics()
837 838 self.register_magics(TerminalMagics)
838 839
839 840 def init_alias(self):
840 841 # The parent class defines aliases that can be safely used with any
841 842 # frontend.
842 843 super(TerminalInteractiveShell, self).init_alias()
843 844
844 845 # Now define aliases that only make sense on the terminal, because they
845 846 # need direct access to the console in a way that we can't emulate in
846 847 # GUI or web frontend
847 848 if os.name == 'posix':
848 849 for cmd in ('clear', 'more', 'less', 'man'):
849 850 self.alias_manager.soft_define_alias(cmd, cmd)
850 851
851 852
852 853 def __init__(self, *args, **kwargs) -> None:
853 854 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
854 855 self._set_autosuggestions(self.autosuggestions_provider)
855 856 self.init_prompt_toolkit_cli()
856 857 self.init_term_title()
857 858 self.keep_running = True
858 859 self._set_formatter(self.autoformatter)
859 860
860 861
861 862 def ask_exit(self):
862 863 self.keep_running = False
863 864
864 865 rl_next_input = None
865 866
866 867 def interact(self):
867 868 self.keep_running = True
868 869 while self.keep_running:
869 870 print(self.separate_in, end='')
870 871
871 872 try:
872 873 code = self.prompt_for_code()
873 874 except EOFError:
874 875 if (not self.confirm_exit) \
875 876 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
876 877 self.ask_exit()
877 878
878 879 else:
879 880 if code:
880 881 self.run_cell(code, store_history=True)
881 882
882 883 def mainloop(self):
883 884 # An extra layer of protection in case someone mashing Ctrl-C breaks
884 885 # out of our internal code.
885 886 while True:
886 887 try:
887 888 self.interact()
888 889 break
889 890 except KeyboardInterrupt as e:
890 891 print("\n%s escaped interact()\n" % type(e).__name__)
891 892 finally:
892 893 # An interrupt during the eventloop will mess up the
893 894 # internal state of the prompt_toolkit library.
894 895 # Stopping the eventloop fixes this, see
895 896 # https://github.com/ipython/ipython/pull/9867
896 897 if hasattr(self, '_eventloop'):
897 898 self._eventloop.stop()
898 899
899 900 self.restore_term_title()
900 901
901 902 # try to call some at-exit operation optimistically as some things can't
902 903 # be done during interpreter shutdown. this is technically inaccurate as
903 904 # this make mainlool not re-callable, but that should be a rare if not
904 905 # in existent use case.
905 906
906 907 self._atexit_once()
907 908
908 909
909 910 _inputhook = None
910 911 def inputhook(self, context):
911 912 if self._inputhook is not None:
912 913 self._inputhook(context)
913 914
914 915 active_eventloop = None
915 916 def enable_gui(self, gui=None):
916 917 if self._inputhook is None and gui is None:
917 918 print("No event loop hook running.")
918 919 return
919 920
920 921 if self._inputhook is not None and gui is not None:
921 922 print(
922 923 f"Shell is already running a gui event loop for {self.active_eventloop}. "
923 924 "Call with no arguments to disable the current loop."
924 925 )
925 926 return
926 927 if self._inputhook is not None and gui is None:
927 928 self.active_eventloop = self._inputhook = None
928 929
929 930 if gui and (gui not in {"inline", "webagg"}):
930 931 # This hook runs with each cycle of the `prompt_toolkit`'s event loop.
931 932 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
932 933 else:
933 934 self.active_eventloop = self._inputhook = None
934 935
935 936 # For prompt_toolkit 3.0. We have to create an asyncio event loop with
936 937 # this inputhook.
937 938 if PTK3:
938 939 import asyncio
939 940 from prompt_toolkit.eventloop import new_eventloop_with_inputhook
940 941
941 942 if gui == 'asyncio':
942 943 # When we integrate the asyncio event loop, run the UI in the
943 944 # same event loop as the rest of the code. don't use an actual
944 945 # input hook. (Asyncio is not made for nesting event loops.)
945 946 self.pt_loop = get_asyncio_loop()
946 947 print("Installed asyncio event loop hook.")
947 948
948 949 elif self._inputhook:
949 950 # If an inputhook was set, create a new asyncio event loop with
950 951 # this inputhook for the prompt.
951 952 self.pt_loop = new_eventloop_with_inputhook(self._inputhook)
952 953 print(f"Installed {self.active_eventloop} event loop hook.")
953 954 else:
954 955 # When there's no inputhook, run the prompt in a separate
955 956 # asyncio event loop.
956 957 self.pt_loop = asyncio.new_event_loop()
957 958 print("GUI event loop hook disabled.")
958 959
959 960 # Run !system commands directly, not through pipes, so terminal programs
960 961 # work correctly.
961 962 system = InteractiveShell.system_raw
962 963
963 964 def auto_rewrite_input(self, cmd):
964 965 """Overridden from the parent class to use fancy rewriting prompt"""
965 966 if not self.show_rewritten_input:
966 967 return
967 968
968 969 tokens = self.prompts.rewrite_prompt_tokens()
969 970 if self.pt_app:
970 971 print_formatted_text(PygmentsTokens(tokens), end='',
971 972 style=self.pt_app.app.style)
972 973 print(cmd)
973 974 else:
974 975 prompt = ''.join(s for t, s in tokens)
975 976 print(prompt, cmd, sep='')
976 977
977 978 _prompts_before = None
978 979 def switch_doctest_mode(self, mode):
979 980 """Switch prompts to classic for %doctest_mode"""
980 981 if mode:
981 982 self._prompts_before = self.prompts
982 983 self.prompts = ClassicPrompts(self)
983 984 elif self._prompts_before:
984 985 self.prompts = self._prompts_before
985 986 self._prompts_before = None
986 987 # self._update_layout()
987 988
988 989
989 990 InteractiveShellABC.register(TerminalInteractiveShell)
990 991
991 992 if __name__ == '__main__':
992 993 TerminalInteractiveShell.instance().interact()
@@ -1,601 +1,609 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 os
10 10 import signal
11 11 import sys
12 12 import warnings
13 13 from dataclasses import dataclass
14 14 from typing import Callable, Any, Optional, List
15 15
16 16 from prompt_toolkit.application.current import get_app
17 17 from prompt_toolkit.key_binding import KeyBindings
18 18 from prompt_toolkit.key_binding.key_processor import KeyPressEvent
19 19 from prompt_toolkit.key_binding.bindings import named_commands as nc
20 20 from prompt_toolkit.key_binding.bindings.completion import (
21 21 display_completions_like_readline,
22 22 )
23 23 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
24 24 from prompt_toolkit.filters import Condition
25 25
26 26 from IPython.core.getipython import get_ipython
27 27 from IPython.terminal.shortcuts import auto_match as match
28 28 from IPython.terminal.shortcuts import auto_suggest
29 29 from IPython.terminal.shortcuts.filters import filter_from_string
30 30 from IPython.utils.decorators import undoc
31 31
32 32 from prompt_toolkit.enums import DEFAULT_BUFFER
33 33
34 34 __all__ = ["create_ipython_shortcuts"]
35 35
36 36
37 37 @dataclass
38 38 class BaseBinding:
39 39 command: Callable[[KeyPressEvent], Any]
40 40 keys: List[str]
41 41
42 42
43 43 @dataclass
44 44 class RuntimeBinding(BaseBinding):
45 45 filter: Condition
46 46
47 47
48 48 @dataclass
49 49 class Binding(BaseBinding):
50 50 # while filter could be created by referencing variables directly (rather
51 51 # than created from strings), by using strings we ensure that users will
52 52 # be able to create filters in configuration (e.g. JSON) files too, which
53 53 # also benefits the documentation by enforcing human-readable filter names.
54 54 condition: Optional[str] = None
55 55
56 56 def __post_init__(self):
57 57 if self.condition:
58 58 self.filter = filter_from_string(self.condition)
59 59 else:
60 60 self.filter = None
61 61
62 62
63 63 def create_identifier(handler: Callable):
64 64 parts = handler.__module__.split(".")
65 65 name = handler.__name__
66 66 package = parts[0]
67 67 if len(parts) > 1:
68 68 final_module = parts[-1]
69 69 return f"{package}:{final_module}.{name}"
70 70 else:
71 71 return f"{package}:{name}"
72 72
73 73
74 74 AUTO_MATCH_BINDINGS = [
75 75 *[
76 76 Binding(
77 77 cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
78 78 )
79 79 for key, cmd in match.auto_match_parens.items()
80 80 ],
81 81 *[
82 82 # raw string
83 83 Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
84 84 for key, cmd in match.auto_match_parens_raw_string.items()
85 85 ],
86 86 Binding(
87 87 match.double_quote,
88 88 ['"'],
89 89 "focused_insert"
90 90 " & auto_match"
91 91 " & not_inside_unclosed_string"
92 92 " & preceded_by_paired_double_quotes"
93 93 " & followed_by_closing_paren_or_end",
94 94 ),
95 95 Binding(
96 96 match.single_quote,
97 97 ["'"],
98 98 "focused_insert"
99 99 " & auto_match"
100 100 " & not_inside_unclosed_string"
101 101 " & preceded_by_paired_single_quotes"
102 102 " & followed_by_closing_paren_or_end",
103 103 ),
104 104 Binding(
105 105 match.docstring_double_quotes,
106 106 ['"'],
107 107 "focused_insert"
108 108 " & auto_match"
109 109 " & not_inside_unclosed_string"
110 110 " & preceded_by_two_double_quotes",
111 111 ),
112 112 Binding(
113 113 match.docstring_single_quotes,
114 114 ["'"],
115 115 "focused_insert"
116 116 " & auto_match"
117 117 " & not_inside_unclosed_string"
118 118 " & preceded_by_two_single_quotes",
119 119 ),
120 120 Binding(
121 121 match.skip_over,
122 122 [")"],
123 123 "focused_insert & auto_match & followed_by_closing_round_paren",
124 124 ),
125 125 Binding(
126 126 match.skip_over,
127 127 ["]"],
128 128 "focused_insert & auto_match & followed_by_closing_bracket",
129 129 ),
130 130 Binding(
131 131 match.skip_over,
132 132 ["}"],
133 133 "focused_insert & auto_match & followed_by_closing_brace",
134 134 ),
135 135 Binding(
136 136 match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
137 137 ),
138 138 Binding(
139 139 match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
140 140 ),
141 141 Binding(
142 142 match.delete_pair,
143 143 ["backspace"],
144 144 "focused_insert"
145 145 " & preceded_by_opening_round_paren"
146 146 " & auto_match"
147 147 " & followed_by_closing_round_paren",
148 148 ),
149 149 Binding(
150 150 match.delete_pair,
151 151 ["backspace"],
152 152 "focused_insert"
153 153 " & preceded_by_opening_bracket"
154 154 " & auto_match"
155 155 " & followed_by_closing_bracket",
156 156 ),
157 157 Binding(
158 158 match.delete_pair,
159 159 ["backspace"],
160 160 "focused_insert"
161 161 " & preceded_by_opening_brace"
162 162 " & auto_match"
163 163 " & followed_by_closing_brace",
164 164 ),
165 165 Binding(
166 166 match.delete_pair,
167 167 ["backspace"],
168 168 "focused_insert"
169 169 " & preceded_by_double_quote"
170 170 " & auto_match"
171 171 " & followed_by_double_quote",
172 172 ),
173 173 Binding(
174 174 match.delete_pair,
175 175 ["backspace"],
176 176 "focused_insert"
177 177 " & preceded_by_single_quote"
178 178 " & auto_match"
179 179 " & followed_by_single_quote",
180 180 ),
181 181 ]
182 182
183 183 AUTO_SUGGEST_BINDINGS = [
184 184 Binding(
185 185 auto_suggest.accept_in_vi_insert_mode,
186 186 ["end"],
187 187 "default_buffer_focused & (ebivim | ~vi_insert_mode)",
188 188 ),
189 189 Binding(
190 190 auto_suggest.accept_in_vi_insert_mode,
191 191 ["c-e"],
192 192 "vi_insert_mode & default_buffer_focused & ebivim",
193 193 ),
194 194 Binding(auto_suggest.accept, ["c-f"], "vi_insert_mode & default_buffer_focused"),
195 195 Binding(
196 196 auto_suggest.accept_word,
197 197 ["escape", "f"],
198 198 "vi_insert_mode & default_buffer_focused & ebivim",
199 199 ),
200 200 Binding(
201 201 auto_suggest.accept_token,
202 202 ["c-right"],
203 203 "has_suggestion & default_buffer_focused",
204 204 ),
205 205 Binding(
206 206 auto_suggest.discard,
207 207 ["escape"],
208 208 "has_suggestion & default_buffer_focused & emacs_insert_mode",
209 209 ),
210 210 Binding(
211 211 auto_suggest.swap_autosuggestion_up,
212 212 ["up"],
213 213 "navigable_suggestions"
214 214 " & ~has_line_above"
215 215 " & has_suggestion"
216 216 " & default_buffer_focused",
217 217 ),
218 218 Binding(
219 219 auto_suggest.swap_autosuggestion_down,
220 220 ["down"],
221 221 "navigable_suggestions"
222 222 " & ~has_line_below"
223 223 " & has_suggestion"
224 224 " & default_buffer_focused",
225 225 ),
226 226 Binding(
227 227 auto_suggest.up_and_update_hint,
228 228 ["up"],
229 229 "has_line_above & navigable_suggestions & default_buffer_focused",
230 230 ),
231 231 Binding(
232 232 auto_suggest.down_and_update_hint,
233 233 ["down"],
234 234 "has_line_below & navigable_suggestions & default_buffer_focused",
235 235 ),
236 236 Binding(
237 237 auto_suggest.accept_character,
238 238 ["escape", "right"],
239 239 "has_suggestion & default_buffer_focused",
240 240 ),
241 241 Binding(
242 242 auto_suggest.accept_and_move_cursor_left,
243 243 ["c-left"],
244 244 "has_suggestion & default_buffer_focused",
245 245 ),
246 246 Binding(
247 247 auto_suggest.accept_and_keep_cursor,
248 248 ["c-down"],
249 249 "has_suggestion & default_buffer_focused",
250 250 ),
251 251 Binding(
252 252 auto_suggest.backspace_and_resume_hint,
253 253 ["backspace"],
254 254 "has_suggestion & default_buffer_focused",
255 255 ),
256 Binding(
257 auto_suggest.resume_hinting,
258 ["right"],
259 # For now this binding is inactive (the filter includes `never`).
260 # TODO: remove `never` if we reach a consensus in #13991
261 # TODO: use `emacs_like_insert_mode` once #13991 is in
262 "never & default_buffer_focused & ((vi_insert_mode & ebivim) | emacs_insert_mode)",
263 ),
256 264 ]
257 265
258 266
259 267 SIMPLE_CONTROL_BINDINGS = [
260 268 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
261 269 for key, cmd in {
262 270 "c-a": nc.beginning_of_line,
263 271 "c-b": nc.backward_char,
264 272 "c-k": nc.kill_line,
265 273 "c-w": nc.backward_kill_word,
266 274 "c-y": nc.yank,
267 275 "c-_": nc.undo,
268 276 }.items()
269 277 ]
270 278
271 279
272 280 ALT_AND_COMOBO_CONTROL_BINDINGS = [
273 281 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
274 282 for keys, cmd in {
275 283 # Control Combos
276 284 ("c-x", "c-e"): nc.edit_and_execute,
277 285 ("c-x", "e"): nc.edit_and_execute,
278 286 # Alt
279 287 ("escape", "b"): nc.backward_word,
280 288 ("escape", "c"): nc.capitalize_word,
281 289 ("escape", "d"): nc.kill_word,
282 290 ("escape", "h"): nc.backward_kill_word,
283 291 ("escape", "l"): nc.downcase_word,
284 292 ("escape", "u"): nc.uppercase_word,
285 293 ("escape", "y"): nc.yank_pop,
286 294 ("escape", "."): nc.yank_last_arg,
287 295 }.items()
288 296 ]
289 297
290 298
291 299 def add_binding(bindings: KeyBindings, binding: Binding):
292 300 bindings.add(
293 301 *binding.keys,
294 302 **({"filter": binding.filter} if binding.filter is not None else {}),
295 303 )(binding.command)
296 304
297 305
298 306 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
299 307 """Set up the prompt_toolkit keyboard shortcuts for IPython.
300 308
301 309 Parameters
302 310 ----------
303 311 shell: InteractiveShell
304 312 The current IPython shell Instance
305 313 skip: List[Binding]
306 314 Bindings to skip.
307 315
308 316 Returns
309 317 -------
310 318 KeyBindings
311 319 the keybinding instance for prompt toolkit.
312 320
313 321 """
314 322 kb = KeyBindings()
315 323 skip = skip or []
316 324 for binding in KEY_BINDINGS:
317 325 skip_this_one = False
318 326 for to_skip in skip:
319 327 if (
320 328 to_skip.command == binding.command
321 329 and to_skip.filter == binding.filter
322 330 and to_skip.keys == binding.keys
323 331 ):
324 332 skip_this_one = True
325 333 break
326 334 if skip_this_one:
327 335 continue
328 336 add_binding(kb, binding)
329 337
330 338 def get_input_mode(self):
331 339 app = get_app()
332 340 app.ttimeoutlen = shell.ttimeoutlen
333 341 app.timeoutlen = shell.timeoutlen
334 342
335 343 return self._input_mode
336 344
337 345 def set_input_mode(self, mode):
338 346 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
339 347 cursor = "\x1b[{} q".format(shape)
340 348
341 349 sys.stdout.write(cursor)
342 350 sys.stdout.flush()
343 351
344 352 self._input_mode = mode
345 353
346 354 if shell.editing_mode == "vi" and shell.modal_cursor:
347 355 ViState._input_mode = InputMode.INSERT # type: ignore
348 356 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
349 357
350 358 return kb
351 359
352 360
353 361 def reformat_and_execute(event):
354 362 """Reformat code and execute it"""
355 363 shell = get_ipython()
356 364 reformat_text_before_cursor(
357 365 event.current_buffer, event.current_buffer.document, shell
358 366 )
359 367 event.current_buffer.validate_and_handle()
360 368
361 369
362 370 def reformat_text_before_cursor(buffer, document, shell):
363 371 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
364 372 try:
365 373 formatted_text = shell.reformat_handler(text)
366 374 buffer.insert_text(formatted_text)
367 375 except Exception as e:
368 376 buffer.insert_text(text)
369 377
370 378
371 379 def handle_return_or_newline_or_execute(event):
372 380 shell = get_ipython()
373 381 if getattr(shell, "handle_return", None):
374 382 return shell.handle_return(shell)(event)
375 383 else:
376 384 return newline_or_execute_outer(shell)(event)
377 385
378 386
379 387 def newline_or_execute_outer(shell):
380 388 def newline_or_execute(event):
381 389 """When the user presses return, insert a newline or execute the code."""
382 390 b = event.current_buffer
383 391 d = b.document
384 392
385 393 if b.complete_state:
386 394 cc = b.complete_state.current_completion
387 395 if cc:
388 396 b.apply_completion(cc)
389 397 else:
390 398 b.cancel_completion()
391 399 return
392 400
393 401 # If there's only one line, treat it as if the cursor is at the end.
394 402 # See https://github.com/ipython/ipython/issues/10425
395 403 if d.line_count == 1:
396 404 check_text = d.text
397 405 else:
398 406 check_text = d.text[: d.cursor_position]
399 407 status, indent = shell.check_complete(check_text)
400 408
401 409 # if all we have after the cursor is whitespace: reformat current text
402 410 # before cursor
403 411 after_cursor = d.text[d.cursor_position :]
404 412 reformatted = False
405 413 if not after_cursor.strip():
406 414 reformat_text_before_cursor(b, d, shell)
407 415 reformatted = True
408 416 if not (
409 417 d.on_last_line
410 418 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
411 419 ):
412 420 if shell.autoindent:
413 421 b.insert_text("\n" + indent)
414 422 else:
415 423 b.insert_text("\n")
416 424 return
417 425
418 426 if (status != "incomplete") and b.accept_handler:
419 427 if not reformatted:
420 428 reformat_text_before_cursor(b, d, shell)
421 429 b.validate_and_handle()
422 430 else:
423 431 if shell.autoindent:
424 432 b.insert_text("\n" + indent)
425 433 else:
426 434 b.insert_text("\n")
427 435
428 436 return newline_or_execute
429 437
430 438
431 439 def previous_history_or_previous_completion(event):
432 440 """
433 441 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
434 442
435 443 If completer is open this still select previous completion.
436 444 """
437 445 event.current_buffer.auto_up()
438 446
439 447
440 448 def next_history_or_next_completion(event):
441 449 """
442 450 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
443 451
444 452 If completer is open this still select next completion.
445 453 """
446 454 event.current_buffer.auto_down()
447 455
448 456
449 457 def dismiss_completion(event):
450 458 """Dismiss completion"""
451 459 b = event.current_buffer
452 460 if b.complete_state:
453 461 b.cancel_completion()
454 462
455 463
456 464 def reset_buffer(event):
457 465 """Reset buffer"""
458 466 b = event.current_buffer
459 467 if b.complete_state:
460 468 b.cancel_completion()
461 469 else:
462 470 b.reset()
463 471
464 472
465 473 def reset_search_buffer(event):
466 474 """Reset search buffer"""
467 475 if event.current_buffer.document.text:
468 476 event.current_buffer.reset()
469 477 else:
470 478 event.app.layout.focus(DEFAULT_BUFFER)
471 479
472 480
473 481 def suspend_to_bg(event):
474 482 """Suspend to background"""
475 483 event.app.suspend_to_background()
476 484
477 485
478 486 def quit(event):
479 487 """
480 488 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
481 489
482 490 On platforms that support SIGQUIT, send SIGQUIT to the current process.
483 491 On other platforms, just exit the process with a message.
484 492 """
485 493 sigquit = getattr(signal, "SIGQUIT", None)
486 494 if sigquit is not None:
487 495 os.kill(0, signal.SIGQUIT)
488 496 else:
489 497 sys.exit("Quit")
490 498
491 499
492 500 def indent_buffer(event):
493 501 """Indent buffer"""
494 502 event.current_buffer.insert_text(" " * 4)
495 503
496 504
497 505 def newline_autoindent(event):
498 506 """Insert a newline after the cursor indented appropriately.
499 507
500 508 Fancier version of former ``newline_with_copy_margin`` which should
501 509 compute the correct indentation of the inserted line. That is to say, indent
502 510 by 4 extra space after a function definition, class definition, context
503 511 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
504 512 """
505 513 shell = get_ipython()
506 514 inputsplitter = shell.input_transformer_manager
507 515 b = event.current_buffer
508 516 d = b.document
509 517
510 518 if b.complete_state:
511 519 b.cancel_completion()
512 520 text = d.text[: d.cursor_position] + "\n"
513 521 _, indent = inputsplitter.check_complete(text)
514 522 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
515 523
516 524
517 525 def open_input_in_editor(event):
518 526 """Open code from input in external editor"""
519 527 event.app.current_buffer.open_in_editor()
520 528
521 529
522 530 if sys.platform == "win32":
523 531 from IPython.core.error import TryNext
524 532 from IPython.lib.clipboard import (
525 533 ClipboardEmpty,
526 534 tkinter_clipboard_get,
527 535 win32_clipboard_get,
528 536 )
529 537
530 538 @undoc
531 539 def win_paste(event):
532 540 try:
533 541 text = win32_clipboard_get()
534 542 except TryNext:
535 543 try:
536 544 text = tkinter_clipboard_get()
537 545 except (TryNext, ClipboardEmpty):
538 546 return
539 547 except ClipboardEmpty:
540 548 return
541 549 event.current_buffer.insert_text(text.replace("\t", " " * 4))
542 550
543 551 else:
544 552
545 553 @undoc
546 554 def win_paste(event):
547 555 """Stub used on other platforms"""
548 556 pass
549 557
550 558
551 559 KEY_BINDINGS = [
552 560 Binding(
553 561 handle_return_or_newline_or_execute,
554 562 ["enter"],
555 563 "default_buffer_focused & ~has_selection & insert_mode",
556 564 ),
557 565 Binding(
558 566 reformat_and_execute,
559 567 ["escape", "enter"],
560 568 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
561 569 ),
562 570 Binding(quit, ["c-\\"]),
563 571 Binding(
564 572 previous_history_or_previous_completion,
565 573 ["c-p"],
566 574 "vi_insert_mode & default_buffer_focused",
567 575 ),
568 576 Binding(
569 577 next_history_or_next_completion,
570 578 ["c-n"],
571 579 "vi_insert_mode & default_buffer_focused",
572 580 ),
573 581 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
574 582 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
575 583 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
576 584 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
577 585 Binding(
578 586 indent_buffer,
579 587 ["tab"], # Ctrl+I == Tab
580 588 "default_buffer_focused"
581 589 " & ~has_selection"
582 590 " & insert_mode"
583 591 " & cursor_in_leading_ws",
584 592 ),
585 593 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
586 594 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
587 595 *AUTO_MATCH_BINDINGS,
588 596 *AUTO_SUGGEST_BINDINGS,
589 597 Binding(
590 598 display_completions_like_readline,
591 599 ["c-i"],
592 600 "readline_like_completions"
593 601 " & default_buffer_focused"
594 602 " & ~has_selection"
595 603 " & insert_mode"
596 604 " & ~cursor_in_leading_ws",
597 605 ),
598 606 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
599 607 *SIMPLE_CONTROL_BINDINGS,
600 608 *ALT_AND_COMOBO_CONTROL_BINDINGS,
601 609 ]
@@ -1,375 +1,374 b''
1 1 import re
2 2 import tokenize
3 3 from io import StringIO
4 4 from typing import Callable, List, Optional, Union, Generator, Tuple
5 5
6 6 from prompt_toolkit.buffer import Buffer
7 7 from prompt_toolkit.key_binding import KeyPressEvent
8 8 from prompt_toolkit.key_binding.bindings import named_commands as nc
9 9 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
10 10 from prompt_toolkit.document import Document
11 11 from prompt_toolkit.history import History
12 12 from prompt_toolkit.shortcuts import PromptSession
13 13 from prompt_toolkit.layout.processors import (
14 14 Processor,
15 15 Transformation,
16 16 TransformationInput,
17 17 )
18 18
19 19 from IPython.core.getipython import get_ipython
20 20 from IPython.utils.tokenutil import generate_tokens
21 21
22 22
23 23 def _get_query(document: Document):
24 24 return document.lines[document.cursor_position_row]
25 25
26 26
27 27 class AppendAutoSuggestionInAnyLine(Processor):
28 28 """
29 29 Append the auto suggestion to lines other than the last (appending to the
30 30 last line is natively supported by the prompt toolkit).
31 31 """
32 32
33 33 def __init__(self, style: str = "class:auto-suggestion") -> None:
34 34 self.style = style
35 35
36 36 def apply_transformation(self, ti: TransformationInput) -> Transformation:
37 37 is_last_line = ti.lineno == ti.document.line_count - 1
38 38 is_active_line = ti.lineno == ti.document.cursor_position_row
39 39
40 40 if not is_last_line and is_active_line:
41 41 buffer = ti.buffer_control.buffer
42 42
43 43 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
44 44 suggestion = buffer.suggestion.text
45 45 else:
46 46 suggestion = ""
47 47
48 48 return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
49 49 else:
50 50 return Transformation(fragments=ti.fragments)
51 51
52 52
53 53 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
54 54 """
55 55 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
56 56 suggestion from history. To do so it remembers the current position, but it
57 57 state need to carefully be cleared on the right events.
58 58 """
59 59
60 60 def __init__(
61 61 self,
62 62 ):
63 63 self.skip_lines = 0
64 64 self._connected_apps = []
65 65
66 66 def reset_history_position(self, _: Buffer):
67 67 self.skip_lines = 0
68 68
69 69 def disconnect(self):
70 70 for pt_app in self._connected_apps:
71 71 text_insert_event = pt_app.default_buffer.on_text_insert
72 72 text_insert_event.remove_handler(self.reset_history_position)
73 73
74 74 def connect(self, pt_app: PromptSession):
75 75 self._connected_apps.append(pt_app)
76 76 # note: `on_text_changed` could be used for a bit different behaviour
77 77 # on character deletion (i.e. reseting history position on backspace)
78 78 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
79 79 pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
80 80
81 81 def get_suggestion(
82 82 self, buffer: Buffer, document: Document
83 83 ) -> Optional[Suggestion]:
84 84 text = _get_query(document)
85 85
86 86 if text.strip():
87 87 for suggestion, _ in self._find_next_match(
88 88 text, self.skip_lines, buffer.history
89 89 ):
90 90 return Suggestion(suggestion)
91 91
92 92 return None
93 93
94 94 def _dismiss(self, buffer, *args, **kwargs):
95 95 buffer.suggestion = None
96 96
97 97 def _find_match(
98 98 self, text: str, skip_lines: float, history: History, previous: bool
99 99 ) -> Generator[Tuple[str, float], None, None]:
100 100 """
101 101 text : str
102 102 Text content to find a match for, the user cursor is most of the
103 103 time at the end of this text.
104 104 skip_lines : float
105 105 number of items to skip in the search, this is used to indicate how
106 106 far in the list the user has navigated by pressing up or down.
107 107 The float type is used as the base value is +inf
108 108 history : History
109 109 prompt_toolkit History instance to fetch previous entries from.
110 110 previous : bool
111 111 Direction of the search, whether we are looking previous match
112 112 (True), or next match (False).
113 113
114 114 Yields
115 115 ------
116 116 Tuple with:
117 117 str:
118 118 current suggestion.
119 119 float:
120 120 will actually yield only ints, which is passed back via skip_lines,
121 121 which may be a +inf (float)
122 122
123 123
124 124 """
125 125 line_number = -1
126 126 for string in reversed(list(history.get_strings())):
127 127 for line in reversed(string.splitlines()):
128 128 line_number += 1
129 129 if not previous and line_number < skip_lines:
130 130 continue
131 131 # do not return empty suggestions as these
132 132 # close the auto-suggestion overlay (and are useless)
133 133 if line.startswith(text) and len(line) > len(text):
134 134 yield line[len(text) :], line_number
135 135 if previous and line_number >= skip_lines:
136 136 return
137 137
138 138 def _find_next_match(
139 139 self, text: str, skip_lines: float, history: History
140 140 ) -> Generator[Tuple[str, float], None, None]:
141 141 return self._find_match(text, skip_lines, history, previous=False)
142 142
143 143 def _find_previous_match(self, text: str, skip_lines: float, history: History):
144 144 return reversed(
145 145 list(self._find_match(text, skip_lines, history, previous=True))
146 146 )
147 147
148 148 def up(self, query: str, other_than: str, history: History) -> None:
149 149 for suggestion, line_number in self._find_next_match(
150 150 query, self.skip_lines, history
151 151 ):
152 152 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
153 153 # we want to switch from 'very.b' to 'very.a' because a) if the
154 154 # suggestion equals current text, prompt-toolkit aborts suggesting
155 155 # b) user likely would not be interested in 'very' anyways (they
156 156 # already typed it).
157 157 if query + suggestion != other_than:
158 158 self.skip_lines = line_number
159 159 break
160 160 else:
161 161 # no matches found, cycle back to beginning
162 162 self.skip_lines = 0
163 163
164 164 def down(self, query: str, other_than: str, history: History) -> None:
165 165 for suggestion, line_number in self._find_previous_match(
166 166 query, self.skip_lines, history
167 167 ):
168 168 if query + suggestion != other_than:
169 169 self.skip_lines = line_number
170 170 break
171 171 else:
172 172 # no matches found, cycle to end
173 173 for suggestion, line_number in self._find_previous_match(
174 174 query, float("Inf"), history
175 175 ):
176 176 if query + suggestion != other_than:
177 177 self.skip_lines = line_number
178 178 break
179 179
180 180
181 181 # Needed for to accept autosuggestions in vi insert mode
182 182 def accept_in_vi_insert_mode(event: KeyPressEvent):
183 183 """Apply autosuggestion if at end of line."""
184 184 buffer = event.current_buffer
185 185 d = buffer.document
186 186 after_cursor = d.text[d.cursor_position :]
187 187 lines = after_cursor.split("\n")
188 188 end_of_current_line = lines[0].strip()
189 189 suggestion = buffer.suggestion
190 190 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
191 191 buffer.insert_text(suggestion.text)
192 192 else:
193 193 nc.end_of_line(event)
194 194
195 195
196 196 def accept(event: KeyPressEvent):
197 197 """Accept autosuggestion"""
198 198 buffer = event.current_buffer
199 199 suggestion = buffer.suggestion
200 200 if suggestion:
201 201 buffer.insert_text(suggestion.text)
202 202 else:
203 203 nc.forward_char(event)
204 204
205 205
206 206 def discard(event: KeyPressEvent):
207 207 """Discard autosuggestion"""
208 208 buffer = event.current_buffer
209 209 buffer.suggestion = None
210 210
211 211
212 212 def accept_word(event: KeyPressEvent):
213 213 """Fill partial autosuggestion by word"""
214 214 buffer = event.current_buffer
215 215 suggestion = buffer.suggestion
216 216 if suggestion:
217 217 t = re.split(r"(\S+\s+)", suggestion.text)
218 218 buffer.insert_text(next((x for x in t if x), ""))
219 219 else:
220 220 nc.forward_word(event)
221 221
222 222
223 223 def accept_character(event: KeyPressEvent):
224 224 """Fill partial autosuggestion by character"""
225 225 b = event.current_buffer
226 226 suggestion = b.suggestion
227 227 if suggestion and suggestion.text:
228 228 b.insert_text(suggestion.text[0])
229 229
230 230
231 231 def accept_and_keep_cursor(event: KeyPressEvent):
232 232 """Accept autosuggestion and keep cursor in place"""
233 233 buffer = event.current_buffer
234 234 old_position = buffer.cursor_position
235 235 suggestion = buffer.suggestion
236 236 if suggestion:
237 237 buffer.insert_text(suggestion.text)
238 238 buffer.cursor_position = old_position
239 239
240 240
241 241 def accept_and_move_cursor_left(event: KeyPressEvent):
242 242 """Accept autosuggestion and move cursor left in place"""
243 243 accept_and_keep_cursor(event)
244 244 nc.backward_char(event)
245 245
246 246
247 247 def _update_hint(buffer: Buffer):
248 248 if buffer.auto_suggest:
249 249 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
250 250 buffer.suggestion = suggestion
251 251
252 252
253 253 def backspace_and_resume_hint(event: KeyPressEvent):
254 254 """Resume autosuggestions after deleting last character"""
255 current_buffer = event.current_buffer
255 nc.backward_delete_char(event)
256 _update_hint(event.current_buffer)
256 257
257 def resume_hinting(buffer: Buffer):
258 _update_hint(buffer)
259 current_buffer.on_text_changed.remove_handler(resume_hinting)
260 258
261 current_buffer.on_text_changed.add_handler(resume_hinting)
262 nc.backward_delete_char(event)
259 def resume_hinting(event: KeyPressEvent):
260 """Resume autosuggestions"""
261 return _update_hint(event.current_buffer)
263 262
264 263
265 264 def up_and_update_hint(event: KeyPressEvent):
266 265 """Go up and update hint"""
267 266 current_buffer = event.current_buffer
268 267
269 268 current_buffer.auto_up(count=event.arg)
270 269 _update_hint(current_buffer)
271 270
272 271
273 272 def down_and_update_hint(event: KeyPressEvent):
274 273 """Go down and update hint"""
275 274 current_buffer = event.current_buffer
276 275
277 276 current_buffer.auto_down(count=event.arg)
278 277 _update_hint(current_buffer)
279 278
280 279
281 280 def accept_token(event: KeyPressEvent):
282 281 """Fill partial autosuggestion by token"""
283 282 b = event.current_buffer
284 283 suggestion = b.suggestion
285 284
286 285 if suggestion:
287 286 prefix = _get_query(b.document)
288 287 text = prefix + suggestion.text
289 288
290 289 tokens: List[Optional[str]] = [None, None, None]
291 290 substrings = [""]
292 291 i = 0
293 292
294 293 for token in generate_tokens(StringIO(text).readline):
295 294 if token.type == tokenize.NEWLINE:
296 295 index = len(text)
297 296 else:
298 297 index = text.index(token[1], len(substrings[-1]))
299 298 substrings.append(text[:index])
300 299 tokenized_so_far = substrings[-1]
301 300 if tokenized_so_far.startswith(prefix):
302 301 if i == 0 and len(tokenized_so_far) > len(prefix):
303 302 tokens[0] = tokenized_so_far[len(prefix) :]
304 303 substrings.append(tokenized_so_far)
305 304 i += 1
306 305 tokens[i] = token[1]
307 306 if i == 2:
308 307 break
309 308 i += 1
310 309
311 310 if tokens[0]:
312 311 to_insert: str
313 312 insert_text = substrings[-2]
314 313 if tokens[1] and len(tokens[1]) == 1:
315 314 insert_text = substrings[-1]
316 315 to_insert = insert_text[len(prefix) :]
317 316 b.insert_text(to_insert)
318 317 return
319 318
320 319 nc.forward_word(event)
321 320
322 321
323 322 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
324 323
325 324
326 325 def _swap_autosuggestion(
327 326 buffer: Buffer,
328 327 provider: NavigableAutoSuggestFromHistory,
329 328 direction_method: Callable,
330 329 ):
331 330 """
332 331 We skip most recent history entry (in either direction) if it equals the
333 332 current autosuggestion because if user cycles when auto-suggestion is shown
334 333 they most likely want something else than what was suggested (otherwise
335 334 they would have accepted the suggestion).
336 335 """
337 336 suggestion = buffer.suggestion
338 337 if not suggestion:
339 338 return
340 339
341 340 query = _get_query(buffer.document)
342 341 current = query + suggestion.text
343 342
344 343 direction_method(query=query, other_than=current, history=buffer.history)
345 344
346 345 new_suggestion = provider.get_suggestion(buffer, buffer.document)
347 346 buffer.suggestion = new_suggestion
348 347
349 348
350 349 def swap_autosuggestion_up(event: KeyPressEvent):
351 350 """Get next autosuggestion from history."""
352 351 shell = get_ipython()
353 352 provider = shell.auto_suggest
354 353
355 354 if not isinstance(provider, NavigableAutoSuggestFromHistory):
356 355 return
357 356
358 357 return _swap_autosuggestion(
359 358 buffer=event.current_buffer, provider=provider, direction_method=provider.up
360 359 )
361 360
362 361
363 362 def swap_autosuggestion_down(event: KeyPressEvent):
364 363 """Get previous autosuggestion from history."""
365 364 shell = get_ipython()
366 365 provider = shell.auto_suggest
367 366
368 367 if not isinstance(provider, NavigableAutoSuggestFromHistory):
369 368 return
370 369
371 370 return _swap_autosuggestion(
372 371 buffer=event.current_buffer,
373 372 provider=provider,
374 373 direction_method=provider.down,
375 374 )
@@ -1,256 +1,259 b''
1 1 """
2 2 Filters restricting scope of IPython Terminal shortcuts.
3 3 """
4 4
5 5 # Copyright (c) IPython Development Team.
6 6 # Distributed under the terms of the Modified BSD License.
7 7
8 8 import ast
9 9 import re
10 10 import signal
11 11 import sys
12 12 from typing import Callable, Dict, Union
13 13
14 14 from prompt_toolkit.application.current import get_app
15 15 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
16 16 from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions
17 17 from prompt_toolkit.filters import has_focus as has_focus_impl
18 18 from prompt_toolkit.filters import (
19 19 Always,
20 Never,
20 21 has_selection,
21 22 has_suggestion,
22 23 vi_insert_mode,
23 24 vi_mode,
24 25 )
25 26 from prompt_toolkit.layout.layout import FocusableElement
26 27
27 28 from IPython.core.getipython import get_ipython
28 29 from IPython.core.guarded_eval import _find_dunder, BINARY_OP_DUNDERS, UNARY_OP_DUNDERS
29 30 from IPython.terminal.shortcuts import auto_suggest
30 31 from IPython.utils.decorators import undoc
31 32
32 33
33 34 @undoc
34 35 @Condition
35 36 def cursor_in_leading_ws():
36 37 before = get_app().current_buffer.document.current_line_before_cursor
37 38 return (not before) or before.isspace()
38 39
39 40
40 41 def has_focus(value: FocusableElement):
41 42 """Wrapper around has_focus adding a nice `__name__` to tester function"""
42 43 tester = has_focus_impl(value).func
43 44 tester.__name__ = f"is_focused({value})"
44 45 return Condition(tester)
45 46
46 47
47 48 @undoc
48 49 @Condition
49 50 def has_line_below() -> bool:
50 51 document = get_app().current_buffer.document
51 52 return document.cursor_position_row < len(document.lines) - 1
52 53
53 54
54 55 @undoc
55 56 @Condition
56 57 def has_line_above() -> bool:
57 58 document = get_app().current_buffer.document
58 59 return document.cursor_position_row != 0
59 60
60 61
61 62 @Condition
62 63 def ebivim():
63 64 shell = get_ipython()
64 65 return shell.emacs_bindings_in_vi_insert_mode
65 66
66 67
67 68 @Condition
68 69 def supports_suspend():
69 70 return hasattr(signal, "SIGTSTP")
70 71
71 72
72 73 @Condition
73 74 def auto_match():
74 75 shell = get_ipython()
75 76 return shell.auto_match
76 77
77 78
78 79 def all_quotes_paired(quote, buf):
79 80 paired = True
80 81 i = 0
81 82 while i < len(buf):
82 83 c = buf[i]
83 84 if c == quote:
84 85 paired = not paired
85 86 elif c == "\\":
86 87 i += 1
87 88 i += 1
88 89 return paired
89 90
90 91
91 92 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
92 93 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
93 94
94 95
95 96 def preceding_text(pattern: Union[str, Callable]):
96 97 if pattern in _preceding_text_cache:
97 98 return _preceding_text_cache[pattern]
98 99
99 100 if callable(pattern):
100 101
101 102 def _preceding_text():
102 103 app = get_app()
103 104 before_cursor = app.current_buffer.document.current_line_before_cursor
104 105 # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
105 106 return bool(pattern(before_cursor)) # type: ignore[operator]
106 107
107 108 else:
108 109 m = re.compile(pattern)
109 110
110 111 def _preceding_text():
111 112 app = get_app()
112 113 before_cursor = app.current_buffer.document.current_line_before_cursor
113 114 return bool(m.match(before_cursor))
114 115
115 116 _preceding_text.__name__ = f"preceding_text({pattern!r})"
116 117
117 118 condition = Condition(_preceding_text)
118 119 _preceding_text_cache[pattern] = condition
119 120 return condition
120 121
121 122
122 123 def following_text(pattern):
123 124 try:
124 125 return _following_text_cache[pattern]
125 126 except KeyError:
126 127 pass
127 128 m = re.compile(pattern)
128 129
129 130 def _following_text():
130 131 app = get_app()
131 132 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
132 133
133 134 _following_text.__name__ = f"following_text({pattern!r})"
134 135
135 136 condition = Condition(_following_text)
136 137 _following_text_cache[pattern] = condition
137 138 return condition
138 139
139 140
140 141 @Condition
141 142 def not_inside_unclosed_string():
142 143 app = get_app()
143 144 s = app.current_buffer.document.text_before_cursor
144 145 # remove escaped quotes
145 146 s = s.replace('\\"', "").replace("\\'", "")
146 147 # remove triple-quoted string literals
147 148 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
148 149 # remove single-quoted string literals
149 150 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
150 151 return not ('"' in s or "'" in s)
151 152
152 153
153 154 @Condition
154 155 def navigable_suggestions():
155 156 shell = get_ipython()
156 157 return isinstance(shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory)
157 158
158 159
159 160 @Condition
160 161 def readline_like_completions():
161 162 shell = get_ipython()
162 163 return shell.display_completions == "readlinelike"
163 164
164 165
165 166 @Condition
166 167 def is_windows_os():
167 168 return sys.platform == "win32"
168 169
169 170
170 171 # these one is callable and re-used multiple times hence needs to be
171 172 # only defined once beforhand so that transforming back to human-readable
172 173 # names works well in the documentation.
173 174 default_buffer_focused = has_focus(DEFAULT_BUFFER)
174 175
175 176 KEYBINDING_FILTERS = {
176 177 "always": Always(),
178 # never is used for exposing commands which have no default keybindings
179 "never": Never(),
177 180 "has_line_below": has_line_below,
178 181 "has_line_above": has_line_above,
179 182 "has_selection": has_selection,
180 183 "has_suggestion": has_suggestion,
181 184 "vi_mode": vi_mode,
182 185 "vi_insert_mode": vi_insert_mode,
183 186 "emacs_insert_mode": emacs_insert_mode,
184 187 "has_completions": has_completions,
185 188 "insert_mode": vi_insert_mode | emacs_insert_mode,
186 189 "default_buffer_focused": default_buffer_focused,
187 190 "search_buffer_focused": has_focus(SEARCH_BUFFER),
188 191 "ebivim": ebivim,
189 192 "supports_suspend": supports_suspend,
190 193 "is_windows_os": is_windows_os,
191 194 "auto_match": auto_match,
192 195 "focused_insert": (vi_insert_mode | emacs_insert_mode) & default_buffer_focused,
193 196 "not_inside_unclosed_string": not_inside_unclosed_string,
194 197 "readline_like_completions": readline_like_completions,
195 198 "preceded_by_paired_double_quotes": preceding_text(
196 199 lambda line: all_quotes_paired('"', line)
197 200 ),
198 201 "preceded_by_paired_single_quotes": preceding_text(
199 202 lambda line: all_quotes_paired("'", line)
200 203 ),
201 204 "preceded_by_raw_str_prefix": preceding_text(r".*(r|R)[\"'](-*)$"),
202 205 "preceded_by_two_double_quotes": preceding_text(r'^.*""$'),
203 206 "preceded_by_two_single_quotes": preceding_text(r"^.*''$"),
204 207 "followed_by_closing_paren_or_end": following_text(r"[,)}\]]|$"),
205 208 "preceded_by_opening_round_paren": preceding_text(r".*\($"),
206 209 "preceded_by_opening_bracket": preceding_text(r".*\[$"),
207 210 "preceded_by_opening_brace": preceding_text(r".*\{$"),
208 211 "preceded_by_double_quote": preceding_text('.*"$'),
209 212 "preceded_by_single_quote": preceding_text(r".*'$"),
210 213 "followed_by_closing_round_paren": following_text(r"^\)"),
211 214 "followed_by_closing_bracket": following_text(r"^\]"),
212 215 "followed_by_closing_brace": following_text(r"^\}"),
213 216 "followed_by_double_quote": following_text('^"'),
214 217 "followed_by_single_quote": following_text("^'"),
215 218 "navigable_suggestions": navigable_suggestions,
216 219 "cursor_in_leading_ws": cursor_in_leading_ws,
217 220 }
218 221
219 222
220 223 def eval_node(node: Union[ast.AST, None]):
221 224 if node is None:
222 225 return None
223 226 if isinstance(node, ast.Expression):
224 227 return eval_node(node.body)
225 228 if isinstance(node, ast.BinOp):
226 229 left = eval_node(node.left)
227 230 right = eval_node(node.right)
228 231 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
229 232 if dunders:
230 233 return getattr(left, dunders[0])(right)
231 234 raise ValueError(f"Unknown binary operation: {node.op}")
232 235 if isinstance(node, ast.UnaryOp):
233 236 value = eval_node(node.operand)
234 237 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
235 238 if dunders:
236 239 return getattr(value, dunders[0])()
237 240 raise ValueError(f"Unknown unary operation: {node.op}")
238 241 if isinstance(node, ast.Name):
239 242 if node.id in KEYBINDING_FILTERS:
240 243 return KEYBINDING_FILTERS[node.id]
241 244 else:
242 245 sep = "\n - "
243 246 known_filters = sep.join(sorted(KEYBINDING_FILTERS))
244 247 raise NameError(
245 248 f"{node.id} is not a known shortcut filter."
246 249 f" Known filters are: {sep}{known_filters}."
247 250 )
248 251 raise ValueError("Unhandled node", ast.dump(node))
249 252
250 253
251 254 def filter_from_string(code: str):
252 255 expression = ast.parse(code, mode="eval")
253 256 return eval_node(expression)
254 257
255 258
256 259 __all__ = ["KEYBINDING_FILTERS", "filter_from_string"]
General Comments 0
You need to be logged in to leave comments. Login now