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