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