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