##// END OF EJS Templates
Expose `auto_suggest.resume_hinting`, fix resume on backspace (#13994)...
Matthias Bussonnier -
r28199:a7d8defd merge
parent child Browse files
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,622 +1,630 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 # there are two reasons for re-defining bindings defined upstream:
185 185 # 1) prompt-toolkit does not execute autosuggestion bindings in vi mode,
186 186 # 2) prompt-toolkit checks if we are at the end of text, not end of line
187 187 # hence it does not work in multi-line mode of navigable provider
188 188 Binding(
189 189 auto_suggest.accept_or_jump_to_end,
190 190 ["end"],
191 191 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
192 192 ),
193 193 Binding(
194 194 auto_suggest.accept_or_jump_to_end,
195 195 ["c-e"],
196 196 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
197 197 ),
198 198 Binding(
199 199 auto_suggest.accept,
200 200 ["c-f"],
201 201 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
202 202 ),
203 203 Binding(
204 204 auto_suggest.accept,
205 205 ["right"],
206 206 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
207 207 ),
208 208 Binding(
209 209 auto_suggest.accept_word,
210 210 ["escape", "f"],
211 211 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
212 212 ),
213 213 Binding(
214 214 auto_suggest.accept_token,
215 215 ["c-right"],
216 216 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
217 217 ),
218 218 Binding(
219 219 auto_suggest.discard,
220 220 ["escape"],
221 221 # note this one is using `emacs_insert_mode`, not `emacs_like_insert_mode`
222 222 # as in `vi_insert_mode` we do not want `escape` to be shadowed (ever).
223 223 "has_suggestion & default_buffer_focused & emacs_insert_mode",
224 224 ),
225 225 Binding(
226 226 auto_suggest.discard,
227 227 ["delete"],
228 228 "has_suggestion & default_buffer_focused & emacs_insert_mode",
229 229 ),
230 230 Binding(
231 231 auto_suggest.swap_autosuggestion_up,
232 232 ["up"],
233 233 "navigable_suggestions"
234 234 " & ~has_line_above"
235 235 " & has_suggestion"
236 236 " & default_buffer_focused",
237 237 ),
238 238 Binding(
239 239 auto_suggest.swap_autosuggestion_down,
240 240 ["down"],
241 241 "navigable_suggestions"
242 242 " & ~has_line_below"
243 243 " & has_suggestion"
244 244 " & default_buffer_focused",
245 245 ),
246 246 Binding(
247 247 auto_suggest.up_and_update_hint,
248 248 ["up"],
249 249 "has_line_above & navigable_suggestions & default_buffer_focused",
250 250 ),
251 251 Binding(
252 252 auto_suggest.down_and_update_hint,
253 253 ["down"],
254 254 "has_line_below & navigable_suggestions & default_buffer_focused",
255 255 ),
256 256 Binding(
257 257 auto_suggest.accept_character,
258 258 ["escape", "right"],
259 259 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
260 260 ),
261 261 Binding(
262 262 auto_suggest.accept_and_move_cursor_left,
263 263 ["c-left"],
264 264 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
265 265 ),
266 266 Binding(
267 267 auto_suggest.accept_and_keep_cursor,
268 268 ["c-down"],
269 269 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
270 270 ),
271 271 Binding(
272 272 auto_suggest.backspace_and_resume_hint,
273 273 ["backspace"],
274 274 # no `has_suggestion` here to allow resuming if no suggestion
275 275 "default_buffer_focused & emacs_like_insert_mode",
276 276 ),
277 Binding(
278 auto_suggest.resume_hinting,
279 ["right"],
280 # For now this binding is inactive (the filter includes `never`).
281 # TODO: remove `never` if we reach a consensus in #13991
282 # TODO: use `emacs_like_insert_mode` once #13991 is in
283 "never & default_buffer_focused & ((vi_insert_mode & ebivim) | emacs_insert_mode)",
284 ),
277 285 ]
278 286
279 287
280 288 SIMPLE_CONTROL_BINDINGS = [
281 289 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
282 290 for key, cmd in {
283 291 "c-a": nc.beginning_of_line,
284 292 "c-b": nc.backward_char,
285 293 "c-k": nc.kill_line,
286 294 "c-w": nc.backward_kill_word,
287 295 "c-y": nc.yank,
288 296 "c-_": nc.undo,
289 297 }.items()
290 298 ]
291 299
292 300
293 301 ALT_AND_COMOBO_CONTROL_BINDINGS = [
294 302 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
295 303 for keys, cmd in {
296 304 # Control Combos
297 305 ("c-x", "c-e"): nc.edit_and_execute,
298 306 ("c-x", "e"): nc.edit_and_execute,
299 307 # Alt
300 308 ("escape", "b"): nc.backward_word,
301 309 ("escape", "c"): nc.capitalize_word,
302 310 ("escape", "d"): nc.kill_word,
303 311 ("escape", "h"): nc.backward_kill_word,
304 312 ("escape", "l"): nc.downcase_word,
305 313 ("escape", "u"): nc.uppercase_word,
306 314 ("escape", "y"): nc.yank_pop,
307 315 ("escape", "."): nc.yank_last_arg,
308 316 }.items()
309 317 ]
310 318
311 319
312 320 def add_binding(bindings: KeyBindings, binding: Binding):
313 321 bindings.add(
314 322 *binding.keys,
315 323 **({"filter": binding.filter} if binding.filter is not None else {}),
316 324 )(binding.command)
317 325
318 326
319 327 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
320 328 """Set up the prompt_toolkit keyboard shortcuts for IPython.
321 329
322 330 Parameters
323 331 ----------
324 332 shell: InteractiveShell
325 333 The current IPython shell Instance
326 334 skip: List[Binding]
327 335 Bindings to skip.
328 336
329 337 Returns
330 338 -------
331 339 KeyBindings
332 340 the keybinding instance for prompt toolkit.
333 341
334 342 """
335 343 kb = KeyBindings()
336 344 skip = skip or []
337 345 for binding in KEY_BINDINGS:
338 346 skip_this_one = False
339 347 for to_skip in skip:
340 348 if (
341 349 to_skip.command == binding.command
342 350 and to_skip.filter == binding.filter
343 351 and to_skip.keys == binding.keys
344 352 ):
345 353 skip_this_one = True
346 354 break
347 355 if skip_this_one:
348 356 continue
349 357 add_binding(kb, binding)
350 358
351 359 def get_input_mode(self):
352 360 app = get_app()
353 361 app.ttimeoutlen = shell.ttimeoutlen
354 362 app.timeoutlen = shell.timeoutlen
355 363
356 364 return self._input_mode
357 365
358 366 def set_input_mode(self, mode):
359 367 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
360 368 cursor = "\x1b[{} q".format(shape)
361 369
362 370 sys.stdout.write(cursor)
363 371 sys.stdout.flush()
364 372
365 373 self._input_mode = mode
366 374
367 375 if shell.editing_mode == "vi" and shell.modal_cursor:
368 376 ViState._input_mode = InputMode.INSERT # type: ignore
369 377 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
370 378
371 379 return kb
372 380
373 381
374 382 def reformat_and_execute(event):
375 383 """Reformat code and execute it"""
376 384 shell = get_ipython()
377 385 reformat_text_before_cursor(
378 386 event.current_buffer, event.current_buffer.document, shell
379 387 )
380 388 event.current_buffer.validate_and_handle()
381 389
382 390
383 391 def reformat_text_before_cursor(buffer, document, shell):
384 392 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
385 393 try:
386 394 formatted_text = shell.reformat_handler(text)
387 395 buffer.insert_text(formatted_text)
388 396 except Exception as e:
389 397 buffer.insert_text(text)
390 398
391 399
392 400 def handle_return_or_newline_or_execute(event):
393 401 shell = get_ipython()
394 402 if getattr(shell, "handle_return", None):
395 403 return shell.handle_return(shell)(event)
396 404 else:
397 405 return newline_or_execute_outer(shell)(event)
398 406
399 407
400 408 def newline_or_execute_outer(shell):
401 409 def newline_or_execute(event):
402 410 """When the user presses return, insert a newline or execute the code."""
403 411 b = event.current_buffer
404 412 d = b.document
405 413
406 414 if b.complete_state:
407 415 cc = b.complete_state.current_completion
408 416 if cc:
409 417 b.apply_completion(cc)
410 418 else:
411 419 b.cancel_completion()
412 420 return
413 421
414 422 # If there's only one line, treat it as if the cursor is at the end.
415 423 # See https://github.com/ipython/ipython/issues/10425
416 424 if d.line_count == 1:
417 425 check_text = d.text
418 426 else:
419 427 check_text = d.text[: d.cursor_position]
420 428 status, indent = shell.check_complete(check_text)
421 429
422 430 # if all we have after the cursor is whitespace: reformat current text
423 431 # before cursor
424 432 after_cursor = d.text[d.cursor_position :]
425 433 reformatted = False
426 434 if not after_cursor.strip():
427 435 reformat_text_before_cursor(b, d, shell)
428 436 reformatted = True
429 437 if not (
430 438 d.on_last_line
431 439 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
432 440 ):
433 441 if shell.autoindent:
434 442 b.insert_text("\n" + indent)
435 443 else:
436 444 b.insert_text("\n")
437 445 return
438 446
439 447 if (status != "incomplete") and b.accept_handler:
440 448 if not reformatted:
441 449 reformat_text_before_cursor(b, d, shell)
442 450 b.validate_and_handle()
443 451 else:
444 452 if shell.autoindent:
445 453 b.insert_text("\n" + indent)
446 454 else:
447 455 b.insert_text("\n")
448 456
449 457 return newline_or_execute
450 458
451 459
452 460 def previous_history_or_previous_completion(event):
453 461 """
454 462 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
455 463
456 464 If completer is open this still select previous completion.
457 465 """
458 466 event.current_buffer.auto_up()
459 467
460 468
461 469 def next_history_or_next_completion(event):
462 470 """
463 471 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
464 472
465 473 If completer is open this still select next completion.
466 474 """
467 475 event.current_buffer.auto_down()
468 476
469 477
470 478 def dismiss_completion(event):
471 479 """Dismiss completion"""
472 480 b = event.current_buffer
473 481 if b.complete_state:
474 482 b.cancel_completion()
475 483
476 484
477 485 def reset_buffer(event):
478 486 """Reset buffer"""
479 487 b = event.current_buffer
480 488 if b.complete_state:
481 489 b.cancel_completion()
482 490 else:
483 491 b.reset()
484 492
485 493
486 494 def reset_search_buffer(event):
487 495 """Reset search buffer"""
488 496 if event.current_buffer.document.text:
489 497 event.current_buffer.reset()
490 498 else:
491 499 event.app.layout.focus(DEFAULT_BUFFER)
492 500
493 501
494 502 def suspend_to_bg(event):
495 503 """Suspend to background"""
496 504 event.app.suspend_to_background()
497 505
498 506
499 507 def quit(event):
500 508 """
501 509 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
502 510
503 511 On platforms that support SIGQUIT, send SIGQUIT to the current process.
504 512 On other platforms, just exit the process with a message.
505 513 """
506 514 sigquit = getattr(signal, "SIGQUIT", None)
507 515 if sigquit is not None:
508 516 os.kill(0, signal.SIGQUIT)
509 517 else:
510 518 sys.exit("Quit")
511 519
512 520
513 521 def indent_buffer(event):
514 522 """Indent buffer"""
515 523 event.current_buffer.insert_text(" " * 4)
516 524
517 525
518 526 def newline_autoindent(event):
519 527 """Insert a newline after the cursor indented appropriately.
520 528
521 529 Fancier version of former ``newline_with_copy_margin`` which should
522 530 compute the correct indentation of the inserted line. That is to say, indent
523 531 by 4 extra space after a function definition, class definition, context
524 532 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
525 533 """
526 534 shell = get_ipython()
527 535 inputsplitter = shell.input_transformer_manager
528 536 b = event.current_buffer
529 537 d = b.document
530 538
531 539 if b.complete_state:
532 540 b.cancel_completion()
533 541 text = d.text[: d.cursor_position] + "\n"
534 542 _, indent = inputsplitter.check_complete(text)
535 543 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
536 544
537 545
538 546 def open_input_in_editor(event):
539 547 """Open code from input in external editor"""
540 548 event.app.current_buffer.open_in_editor()
541 549
542 550
543 551 if sys.platform == "win32":
544 552 from IPython.core.error import TryNext
545 553 from IPython.lib.clipboard import (
546 554 ClipboardEmpty,
547 555 tkinter_clipboard_get,
548 556 win32_clipboard_get,
549 557 )
550 558
551 559 @undoc
552 560 def win_paste(event):
553 561 try:
554 562 text = win32_clipboard_get()
555 563 except TryNext:
556 564 try:
557 565 text = tkinter_clipboard_get()
558 566 except (TryNext, ClipboardEmpty):
559 567 return
560 568 except ClipboardEmpty:
561 569 return
562 570 event.current_buffer.insert_text(text.replace("\t", " " * 4))
563 571
564 572 else:
565 573
566 574 @undoc
567 575 def win_paste(event):
568 576 """Stub used on other platforms"""
569 577 pass
570 578
571 579
572 580 KEY_BINDINGS = [
573 581 Binding(
574 582 handle_return_or_newline_or_execute,
575 583 ["enter"],
576 584 "default_buffer_focused & ~has_selection & insert_mode",
577 585 ),
578 586 Binding(
579 587 reformat_and_execute,
580 588 ["escape", "enter"],
581 589 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
582 590 ),
583 591 Binding(quit, ["c-\\"]),
584 592 Binding(
585 593 previous_history_or_previous_completion,
586 594 ["c-p"],
587 595 "vi_insert_mode & default_buffer_focused",
588 596 ),
589 597 Binding(
590 598 next_history_or_next_completion,
591 599 ["c-n"],
592 600 "vi_insert_mode & default_buffer_focused",
593 601 ),
594 602 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
595 603 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
596 604 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
597 605 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
598 606 Binding(
599 607 indent_buffer,
600 608 ["tab"], # Ctrl+I == Tab
601 609 "default_buffer_focused"
602 610 " & ~has_selection"
603 611 " & insert_mode"
604 612 " & cursor_in_leading_ws",
605 613 ),
606 614 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
607 615 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
608 616 *AUTO_MATCH_BINDINGS,
609 617 *AUTO_SUGGEST_BINDINGS,
610 618 Binding(
611 619 display_completions_like_readline,
612 620 ["c-i"],
613 621 "readline_like_completions"
614 622 " & default_buffer_focused"
615 623 " & ~has_selection"
616 624 " & insert_mode"
617 625 " & ~cursor_in_leading_ws",
618 626 ),
619 627 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
620 628 *SIMPLE_CONTROL_BINDINGS,
621 629 *ALT_AND_COMOBO_CONTROL_BINDINGS,
622 630 ]
@@ -1,397 +1,396 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 import warnings
6 6
7 7 from prompt_toolkit.buffer import Buffer
8 8 from prompt_toolkit.key_binding import KeyPressEvent
9 9 from prompt_toolkit.key_binding.bindings import named_commands as nc
10 10 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
11 11 from prompt_toolkit.document import Document
12 12 from prompt_toolkit.history import History
13 13 from prompt_toolkit.shortcuts import PromptSession
14 14 from prompt_toolkit.layout.processors import (
15 15 Processor,
16 16 Transformation,
17 17 TransformationInput,
18 18 )
19 19
20 20 from IPython.core.getipython import get_ipython
21 21 from IPython.utils.tokenutil import generate_tokens
22 22
23 23
24 24 def _get_query(document: Document):
25 25 return document.lines[document.cursor_position_row]
26 26
27 27
28 28 class AppendAutoSuggestionInAnyLine(Processor):
29 29 """
30 30 Append the auto suggestion to lines other than the last (appending to the
31 31 last line is natively supported by the prompt toolkit).
32 32 """
33 33
34 34 def __init__(self, style: str = "class:auto-suggestion") -> None:
35 35 self.style = style
36 36
37 37 def apply_transformation(self, ti: TransformationInput) -> Transformation:
38 38 is_last_line = ti.lineno == ti.document.line_count - 1
39 39 is_active_line = ti.lineno == ti.document.cursor_position_row
40 40
41 41 if not is_last_line and is_active_line:
42 42 buffer = ti.buffer_control.buffer
43 43
44 44 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
45 45 suggestion = buffer.suggestion.text
46 46 else:
47 47 suggestion = ""
48 48
49 49 return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
50 50 else:
51 51 return Transformation(fragments=ti.fragments)
52 52
53 53
54 54 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
55 55 """
56 56 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
57 57 suggestion from history. To do so it remembers the current position, but it
58 58 state need to carefully be cleared on the right events.
59 59 """
60 60
61 61 def __init__(
62 62 self,
63 63 ):
64 64 self.skip_lines = 0
65 65 self._connected_apps = []
66 66
67 67 def reset_history_position(self, _: Buffer):
68 68 self.skip_lines = 0
69 69
70 70 def disconnect(self):
71 71 for pt_app in self._connected_apps:
72 72 text_insert_event = pt_app.default_buffer.on_text_insert
73 73 text_insert_event.remove_handler(self.reset_history_position)
74 74
75 75 def connect(self, pt_app: PromptSession):
76 76 self._connected_apps.append(pt_app)
77 77 # note: `on_text_changed` could be used for a bit different behaviour
78 78 # on character deletion (i.e. reseting history position on backspace)
79 79 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
80 80 pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
81 81
82 82 def get_suggestion(
83 83 self, buffer: Buffer, document: Document
84 84 ) -> Optional[Suggestion]:
85 85 text = _get_query(document)
86 86
87 87 if text.strip():
88 88 for suggestion, _ in self._find_next_match(
89 89 text, self.skip_lines, buffer.history
90 90 ):
91 91 return Suggestion(suggestion)
92 92
93 93 return None
94 94
95 95 def _dismiss(self, buffer, *args, **kwargs):
96 96 buffer.suggestion = None
97 97
98 98 def _find_match(
99 99 self, text: str, skip_lines: float, history: History, previous: bool
100 100 ) -> Generator[Tuple[str, float], None, None]:
101 101 """
102 102 text : str
103 103 Text content to find a match for, the user cursor is most of the
104 104 time at the end of this text.
105 105 skip_lines : float
106 106 number of items to skip in the search, this is used to indicate how
107 107 far in the list the user has navigated by pressing up or down.
108 108 The float type is used as the base value is +inf
109 109 history : History
110 110 prompt_toolkit History instance to fetch previous entries from.
111 111 previous : bool
112 112 Direction of the search, whether we are looking previous match
113 113 (True), or next match (False).
114 114
115 115 Yields
116 116 ------
117 117 Tuple with:
118 118 str:
119 119 current suggestion.
120 120 float:
121 121 will actually yield only ints, which is passed back via skip_lines,
122 122 which may be a +inf (float)
123 123
124 124
125 125 """
126 126 line_number = -1
127 127 for string in reversed(list(history.get_strings())):
128 128 for line in reversed(string.splitlines()):
129 129 line_number += 1
130 130 if not previous and line_number < skip_lines:
131 131 continue
132 132 # do not return empty suggestions as these
133 133 # close the auto-suggestion overlay (and are useless)
134 134 if line.startswith(text) and len(line) > len(text):
135 135 yield line[len(text) :], line_number
136 136 if previous and line_number >= skip_lines:
137 137 return
138 138
139 139 def _find_next_match(
140 140 self, text: str, skip_lines: float, history: History
141 141 ) -> Generator[Tuple[str, float], None, None]:
142 142 return self._find_match(text, skip_lines, history, previous=False)
143 143
144 144 def _find_previous_match(self, text: str, skip_lines: float, history: History):
145 145 return reversed(
146 146 list(self._find_match(text, skip_lines, history, previous=True))
147 147 )
148 148
149 149 def up(self, query: str, other_than: str, history: History) -> None:
150 150 for suggestion, line_number in self._find_next_match(
151 151 query, self.skip_lines, history
152 152 ):
153 153 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
154 154 # we want to switch from 'very.b' to 'very.a' because a) if the
155 155 # suggestion equals current text, prompt-toolkit aborts suggesting
156 156 # b) user likely would not be interested in 'very' anyways (they
157 157 # already typed it).
158 158 if query + suggestion != other_than:
159 159 self.skip_lines = line_number
160 160 break
161 161 else:
162 162 # no matches found, cycle back to beginning
163 163 self.skip_lines = 0
164 164
165 165 def down(self, query: str, other_than: str, history: History) -> None:
166 166 for suggestion, line_number in self._find_previous_match(
167 167 query, self.skip_lines, history
168 168 ):
169 169 if query + suggestion != other_than:
170 170 self.skip_lines = line_number
171 171 break
172 172 else:
173 173 # no matches found, cycle to end
174 174 for suggestion, line_number in self._find_previous_match(
175 175 query, float("Inf"), history
176 176 ):
177 177 if query + suggestion != other_than:
178 178 self.skip_lines = line_number
179 179 break
180 180
181 181
182 182 def accept_or_jump_to_end(event: KeyPressEvent):
183 183 """Apply autosuggestion or jump to 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 _deprected_accept_in_vi_insert_mode(event: KeyPressEvent):
197 197 """Accept autosuggestion or jump to end of line.
198 198
199 199 .. deprecated:: 8.12
200 200 Use `accept_or_jump_to_end` instead.
201 201 """
202 202 return accept_or_jump_to_end(event)
203 203
204 204
205 205 def accept(event: KeyPressEvent):
206 206 """Accept autosuggestion"""
207 207 buffer = event.current_buffer
208 208 suggestion = buffer.suggestion
209 209 if suggestion:
210 210 buffer.insert_text(suggestion.text)
211 211 else:
212 212 nc.forward_char(event)
213 213
214 214
215 215 def discard(event: KeyPressEvent):
216 216 """Discard autosuggestion"""
217 217 buffer = event.current_buffer
218 218 buffer.suggestion = None
219 219
220 220
221 221 def accept_word(event: KeyPressEvent):
222 222 """Fill partial autosuggestion by word"""
223 223 buffer = event.current_buffer
224 224 suggestion = buffer.suggestion
225 225 if suggestion:
226 226 t = re.split(r"(\S+\s+)", suggestion.text)
227 227 buffer.insert_text(next((x for x in t if x), ""))
228 228 else:
229 229 nc.forward_word(event)
230 230
231 231
232 232 def accept_character(event: KeyPressEvent):
233 233 """Fill partial autosuggestion by character"""
234 234 b = event.current_buffer
235 235 suggestion = b.suggestion
236 236 if suggestion and suggestion.text:
237 237 b.insert_text(suggestion.text[0])
238 238
239 239
240 240 def accept_and_keep_cursor(event: KeyPressEvent):
241 241 """Accept autosuggestion and keep cursor in place"""
242 242 buffer = event.current_buffer
243 243 old_position = buffer.cursor_position
244 244 suggestion = buffer.suggestion
245 245 if suggestion:
246 246 buffer.insert_text(suggestion.text)
247 247 buffer.cursor_position = old_position
248 248
249 249
250 250 def accept_and_move_cursor_left(event: KeyPressEvent):
251 251 """Accept autosuggestion and move cursor left in place"""
252 252 accept_and_keep_cursor(event)
253 253 nc.backward_char(event)
254 254
255 255
256 256 def _update_hint(buffer: Buffer):
257 257 if buffer.auto_suggest:
258 258 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
259 259 buffer.suggestion = suggestion
260 260
261 261
262 262 def backspace_and_resume_hint(event: KeyPressEvent):
263 263 """Resume autosuggestions after deleting last character"""
264 current_buffer = event.current_buffer
264 nc.backward_delete_char(event)
265 _update_hint(event.current_buffer)
265 266
266 def resume_hinting(buffer: Buffer):
267 _update_hint(buffer)
268 current_buffer.on_text_changed.remove_handler(resume_hinting)
269 267
270 current_buffer.on_text_changed.add_handler(resume_hinting)
271 nc.backward_delete_char(event)
268 def resume_hinting(event: KeyPressEvent):
269 """Resume autosuggestions"""
270 return _update_hint(event.current_buffer)
272 271
273 272
274 273 def up_and_update_hint(event: KeyPressEvent):
275 274 """Go up and update hint"""
276 275 current_buffer = event.current_buffer
277 276
278 277 current_buffer.auto_up(count=event.arg)
279 278 _update_hint(current_buffer)
280 279
281 280
282 281 def down_and_update_hint(event: KeyPressEvent):
283 282 """Go down and update hint"""
284 283 current_buffer = event.current_buffer
285 284
286 285 current_buffer.auto_down(count=event.arg)
287 286 _update_hint(current_buffer)
288 287
289 288
290 289 def accept_token(event: KeyPressEvent):
291 290 """Fill partial autosuggestion by token"""
292 291 b = event.current_buffer
293 292 suggestion = b.suggestion
294 293
295 294 if suggestion:
296 295 prefix = _get_query(b.document)
297 296 text = prefix + suggestion.text
298 297
299 298 tokens: List[Optional[str]] = [None, None, None]
300 299 substrings = [""]
301 300 i = 0
302 301
303 302 for token in generate_tokens(StringIO(text).readline):
304 303 if token.type == tokenize.NEWLINE:
305 304 index = len(text)
306 305 else:
307 306 index = text.index(token[1], len(substrings[-1]))
308 307 substrings.append(text[:index])
309 308 tokenized_so_far = substrings[-1]
310 309 if tokenized_so_far.startswith(prefix):
311 310 if i == 0 and len(tokenized_so_far) > len(prefix):
312 311 tokens[0] = tokenized_so_far[len(prefix) :]
313 312 substrings.append(tokenized_so_far)
314 313 i += 1
315 314 tokens[i] = token[1]
316 315 if i == 2:
317 316 break
318 317 i += 1
319 318
320 319 if tokens[0]:
321 320 to_insert: str
322 321 insert_text = substrings[-2]
323 322 if tokens[1] and len(tokens[1]) == 1:
324 323 insert_text = substrings[-1]
325 324 to_insert = insert_text[len(prefix) :]
326 325 b.insert_text(to_insert)
327 326 return
328 327
329 328 nc.forward_word(event)
330 329
331 330
332 331 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
333 332
334 333
335 334 def _swap_autosuggestion(
336 335 buffer: Buffer,
337 336 provider: NavigableAutoSuggestFromHistory,
338 337 direction_method: Callable,
339 338 ):
340 339 """
341 340 We skip most recent history entry (in either direction) if it equals the
342 341 current autosuggestion because if user cycles when auto-suggestion is shown
343 342 they most likely want something else than what was suggested (otherwise
344 343 they would have accepted the suggestion).
345 344 """
346 345 suggestion = buffer.suggestion
347 346 if not suggestion:
348 347 return
349 348
350 349 query = _get_query(buffer.document)
351 350 current = query + suggestion.text
352 351
353 352 direction_method(query=query, other_than=current, history=buffer.history)
354 353
355 354 new_suggestion = provider.get_suggestion(buffer, buffer.document)
356 355 buffer.suggestion = new_suggestion
357 356
358 357
359 358 def swap_autosuggestion_up(event: KeyPressEvent):
360 359 """Get next autosuggestion from history."""
361 360 shell = get_ipython()
362 361 provider = shell.auto_suggest
363 362
364 363 if not isinstance(provider, NavigableAutoSuggestFromHistory):
365 364 return
366 365
367 366 return _swap_autosuggestion(
368 367 buffer=event.current_buffer, provider=provider, direction_method=provider.up
369 368 )
370 369
371 370
372 371 def swap_autosuggestion_down(event: KeyPressEvent):
373 372 """Get previous autosuggestion from history."""
374 373 shell = get_ipython()
375 374 provider = shell.auto_suggest
376 375
377 376 if not isinstance(provider, NavigableAutoSuggestFromHistory):
378 377 return
379 378
380 379 return _swap_autosuggestion(
381 380 buffer=event.current_buffer,
382 381 provider=provider,
383 382 direction_method=provider.down,
384 383 )
385 384
386 385
387 386 def __getattr__(key):
388 387 if key == "accept_in_vi_insert_mode":
389 388 warnings.warn(
390 389 "`accept_in_vi_insert_mode` is deprecated since IPython 8.12 and "
391 390 "renamed to `accept_or_jump_to_end`. Please update your configuration "
392 391 "accordingly",
393 392 DeprecationWarning,
394 393 stacklevel=2,
395 394 )
396 395 return _deprected_accept_in_vi_insert_mode
397 396 raise AttributeError
@@ -1,279 +1,282 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 # https://github.com/ipython/ipython/pull/12603 argued for inclusion of
185 188 # emacs key bindings with a configurable `emacs_bindings_in_vi_insert_mode`
186 189 # toggle; when the toggle is on user can access keybindigns like `ctrl + e`
187 190 # in vi insert mode. Because some of the emacs bindings involve `escape`
188 191 # followed by another key, e.g. `escape` followed by `f`, prompt-toolkit
189 192 # needs to wait to see if there will be another character typed in before
190 193 # executing pure `escape` keybinding; in vi insert mode `escape` switches to
191 194 # command mode which is common and performance critical action for vi users.
192 195 # To avoid the delay users employ a workaround:
193 196 # https://github.com/ipython/ipython/issues/13443#issuecomment-1032753703
194 197 # which involves switching `emacs_bindings_in_vi_insert_mode` off.
195 198 #
196 199 # For the workaround to work:
197 200 # 1) end users need to toggle `emacs_bindings_in_vi_insert_mode` off
198 201 # 2) all keybindings which would involve `escape` need to respect that
199 202 # toggle by including either:
200 203 # - `vi_insert_mode & ebivim` for actions which have emacs keybindings
201 204 # predefined upstream in prompt-toolkit, or
202 205 # - `emacs_like_insert_mode` for actions which do not have existing
203 206 # emacs keybindings predefined upstream (or need overriding of the
204 207 # upstream bindings to modify behaviour), defined below.
205 208 "emacs_like_insert_mode": (vi_insert_mode & ebivim) | emacs_insert_mode,
206 209 "has_completions": has_completions,
207 210 "insert_mode": vi_insert_mode | emacs_insert_mode,
208 211 "default_buffer_focused": default_buffer_focused,
209 212 "search_buffer_focused": has_focus(SEARCH_BUFFER),
210 213 # `ebivim` stands for emacs bindings in vi insert mode
211 214 "ebivim": ebivim,
212 215 "supports_suspend": supports_suspend,
213 216 "is_windows_os": is_windows_os,
214 217 "auto_match": auto_match,
215 218 "focused_insert": (vi_insert_mode | emacs_insert_mode) & default_buffer_focused,
216 219 "not_inside_unclosed_string": not_inside_unclosed_string,
217 220 "readline_like_completions": readline_like_completions,
218 221 "preceded_by_paired_double_quotes": preceding_text(
219 222 lambda line: all_quotes_paired('"', line)
220 223 ),
221 224 "preceded_by_paired_single_quotes": preceding_text(
222 225 lambda line: all_quotes_paired("'", line)
223 226 ),
224 227 "preceded_by_raw_str_prefix": preceding_text(r".*(r|R)[\"'](-*)$"),
225 228 "preceded_by_two_double_quotes": preceding_text(r'^.*""$'),
226 229 "preceded_by_two_single_quotes": preceding_text(r"^.*''$"),
227 230 "followed_by_closing_paren_or_end": following_text(r"[,)}\]]|$"),
228 231 "preceded_by_opening_round_paren": preceding_text(r".*\($"),
229 232 "preceded_by_opening_bracket": preceding_text(r".*\[$"),
230 233 "preceded_by_opening_brace": preceding_text(r".*\{$"),
231 234 "preceded_by_double_quote": preceding_text('.*"$'),
232 235 "preceded_by_single_quote": preceding_text(r".*'$"),
233 236 "followed_by_closing_round_paren": following_text(r"^\)"),
234 237 "followed_by_closing_bracket": following_text(r"^\]"),
235 238 "followed_by_closing_brace": following_text(r"^\}"),
236 239 "followed_by_double_quote": following_text('^"'),
237 240 "followed_by_single_quote": following_text("^'"),
238 241 "navigable_suggestions": navigable_suggestions,
239 242 "cursor_in_leading_ws": cursor_in_leading_ws,
240 243 }
241 244
242 245
243 246 def eval_node(node: Union[ast.AST, None]):
244 247 if node is None:
245 248 return None
246 249 if isinstance(node, ast.Expression):
247 250 return eval_node(node.body)
248 251 if isinstance(node, ast.BinOp):
249 252 left = eval_node(node.left)
250 253 right = eval_node(node.right)
251 254 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
252 255 if dunders:
253 256 return getattr(left, dunders[0])(right)
254 257 raise ValueError(f"Unknown binary operation: {node.op}")
255 258 if isinstance(node, ast.UnaryOp):
256 259 value = eval_node(node.operand)
257 260 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
258 261 if dunders:
259 262 return getattr(value, dunders[0])()
260 263 raise ValueError(f"Unknown unary operation: {node.op}")
261 264 if isinstance(node, ast.Name):
262 265 if node.id in KEYBINDING_FILTERS:
263 266 return KEYBINDING_FILTERS[node.id]
264 267 else:
265 268 sep = "\n - "
266 269 known_filters = sep.join(sorted(KEYBINDING_FILTERS))
267 270 raise NameError(
268 271 f"{node.id} is not a known shortcut filter."
269 272 f" Known filters are: {sep}{known_filters}."
270 273 )
271 274 raise ValueError("Unhandled node", ast.dump(node))
272 275
273 276
274 277 def filter_from_string(code: str):
275 278 expression = ast.parse(code, mode="eval")
276 279 return eval_node(expression)
277 280
278 281
279 282 __all__ = ["KEYBINDING_FILTERS", "filter_from_string"]
General Comments 0
You need to be logged in to leave comments. Login now