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