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