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