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