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