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