##// END OF EJS Templates
Allow to customise shortcuts using a traitlet (#13928)...
Matthias Bussonnier -
r28115:442c33cf merge
parent child Browse files
Show More
@@ -0,0 +1,256 b''
1 """
2 Filters restricting scope of IPython Terminal shortcuts.
3 """
4
5 # Copyright (c) IPython Development Team.
6 # Distributed under the terms of the Modified BSD License.
7
8 import ast
9 import re
10 import signal
11 import sys
12 from typing import Callable, Dict, Union
13
14 from prompt_toolkit.application.current import get_app
15 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
16 from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions
17 from prompt_toolkit.filters import has_focus as has_focus_impl
18 from prompt_toolkit.filters import (
19 Always,
20 has_selection,
21 has_suggestion,
22 vi_insert_mode,
23 vi_mode,
24 )
25 from prompt_toolkit.layout.layout import FocusableElement
26
27 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.terminal.shortcuts import auto_suggest
30 from IPython.utils.decorators import undoc
31
32
33 @undoc
34 @Condition
35 def cursor_in_leading_ws():
36 before = get_app().current_buffer.document.current_line_before_cursor
37 return (not before) or before.isspace()
38
39
40 def has_focus(value: FocusableElement):
41 """Wrapper around has_focus adding a nice `__name__` to tester function"""
42 tester = has_focus_impl(value).func
43 tester.__name__ = f"is_focused({value})"
44 return Condition(tester)
45
46
47 @undoc
48 @Condition
49 def has_line_below() -> bool:
50 document = get_app().current_buffer.document
51 return document.cursor_position_row < len(document.lines) - 1
52
53
54 @undoc
55 @Condition
56 def has_line_above() -> bool:
57 document = get_app().current_buffer.document
58 return document.cursor_position_row != 0
59
60
61 @Condition
62 def ebivim():
63 shell = get_ipython()
64 return shell.emacs_bindings_in_vi_insert_mode
65
66
67 @Condition
68 def supports_suspend():
69 return hasattr(signal, "SIGTSTP")
70
71
72 @Condition
73 def auto_match():
74 shell = get_ipython()
75 return shell.auto_match
76
77
78 def all_quotes_paired(quote, buf):
79 paired = True
80 i = 0
81 while i < len(buf):
82 c = buf[i]
83 if c == quote:
84 paired = not paired
85 elif c == "\\":
86 i += 1
87 i += 1
88 return paired
89
90
91 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
92 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
93
94
95 def preceding_text(pattern: Union[str, Callable]):
96 if pattern in _preceding_text_cache:
97 return _preceding_text_cache[pattern]
98
99 if callable(pattern):
100
101 def _preceding_text():
102 app = get_app()
103 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 return bool(pattern(before_cursor)) # type: ignore[operator]
106
107 else:
108 m = re.compile(pattern)
109
110 def _preceding_text():
111 app = get_app()
112 before_cursor = app.current_buffer.document.current_line_before_cursor
113 return bool(m.match(before_cursor))
114
115 _preceding_text.__name__ = f"preceding_text({pattern!r})"
116
117 condition = Condition(_preceding_text)
118 _preceding_text_cache[pattern] = condition
119 return condition
120
121
122 def following_text(pattern):
123 try:
124 return _following_text_cache[pattern]
125 except KeyError:
126 pass
127 m = re.compile(pattern)
128
129 def _following_text():
130 app = get_app()
131 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
132
133 _following_text.__name__ = f"following_text({pattern!r})"
134
135 condition = Condition(_following_text)
136 _following_text_cache[pattern] = condition
137 return condition
138
139
140 @Condition
141 def not_inside_unclosed_string():
142 app = get_app()
143 s = app.current_buffer.document.text_before_cursor
144 # remove escaped quotes
145 s = s.replace('\\"', "").replace("\\'", "")
146 # remove triple-quoted string literals
147 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
148 # remove single-quoted string literals
149 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
150 return not ('"' in s or "'" in s)
151
152
153 @Condition
154 def navigable_suggestions():
155 shell = get_ipython()
156 return isinstance(shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory)
157
158
159 @Condition
160 def readline_like_completions():
161 shell = get_ipython()
162 return shell.display_completions == "readlinelike"
163
164
165 @Condition
166 def is_windows_os():
167 return sys.platform == "win32"
168
169
170 # 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 # names works well in the documentation.
173 default_buffer_focused = has_focus(DEFAULT_BUFFER)
174
175 KEYBINDING_FILTERS = {
176 "always": Always(),
177 "has_line_below": has_line_below,
178 "has_line_above": has_line_above,
179 "has_selection": has_selection,
180 "has_suggestion": has_suggestion,
181 "vi_mode": vi_mode,
182 "vi_insert_mode": vi_insert_mode,
183 "emacs_insert_mode": emacs_insert_mode,
184 "has_completions": has_completions,
185 "insert_mode": vi_insert_mode | emacs_insert_mode,
186 "default_buffer_focused": default_buffer_focused,
187 "search_buffer_focused": has_focus(SEARCH_BUFFER),
188 "ebivim": ebivim,
189 "supports_suspend": supports_suspend,
190 "is_windows_os": is_windows_os,
191 "auto_match": auto_match,
192 "focused_insert": (vi_insert_mode | emacs_insert_mode) & default_buffer_focused,
193 "not_inside_unclosed_string": not_inside_unclosed_string,
194 "readline_like_completions": readline_like_completions,
195 "preceded_by_paired_double_quotes": preceding_text(
196 lambda line: all_quotes_paired('"', line)
197 ),
198 "preceded_by_paired_single_quotes": preceding_text(
199 lambda line: all_quotes_paired("'", line)
200 ),
201 "preceded_by_raw_str_prefix": preceding_text(r".*(r|R)[\"'](-*)$"),
202 "preceded_by_two_double_quotes": preceding_text(r'^.*""$'),
203 "preceded_by_two_single_quotes": preceding_text(r"^.*''$"),
204 "followed_by_closing_paren_or_end": following_text(r"[,)}\]]|$"),
205 "preceded_by_opening_round_paren": preceding_text(r".*\($"),
206 "preceded_by_opening_bracket": preceding_text(r".*\[$"),
207 "preceded_by_opening_brace": preceding_text(r".*\{$"),
208 "preceded_by_double_quote": preceding_text('.*"$'),
209 "preceded_by_single_quote": preceding_text(r".*'$"),
210 "followed_by_closing_round_paren": following_text(r"^\)"),
211 "followed_by_closing_bracket": following_text(r"^\]"),
212 "followed_by_closing_brace": following_text(r"^\}"),
213 "followed_by_double_quote": following_text('^"'),
214 "followed_by_single_quote": following_text("^'"),
215 "navigable_suggestions": navigable_suggestions,
216 "cursor_in_leading_ws": cursor_in_leading_ws,
217 }
218
219
220 def eval_node(node: Union[ast.AST, None]):
221 if node is None:
222 return None
223 if isinstance(node, ast.Expression):
224 return eval_node(node.body)
225 if isinstance(node, ast.BinOp):
226 left = eval_node(node.left)
227 right = eval_node(node.right)
228 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
229 if dunders:
230 return getattr(left, dunders[0])(right)
231 raise ValueError(f"Unknown binary operation: {node.op}")
232 if isinstance(node, ast.UnaryOp):
233 value = eval_node(node.operand)
234 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
235 if dunders:
236 return getattr(value, dunders[0])()
237 raise ValueError(f"Unknown unary operation: {node.op}")
238 if isinstance(node, ast.Name):
239 if node.id in KEYBINDING_FILTERS:
240 return KEYBINDING_FILTERS[node.id]
241 else:
242 sep = "\n - "
243 known_filters = sep.join(sorted(KEYBINDING_FILTERS))
244 raise NameError(
245 f"{node.id} is not a known shortcut filter."
246 f" Known filters are: {sep}{known_filters}."
247 )
248 raise ValueError("Unhandled node", ast.dump(node))
249
250
251 def filter_from_string(code: str):
252 expression = ast.parse(code, mode="eval")
253 return eval_node(expression)
254
255
256 __all__ = ["KEYBINDING_FILTERS", "filter_from_string"]
@@ -0,0 +1,21 b''
1 Terminal shortcuts customization
2 ================================
3
4 Previously modifying shortcuts was only possible by hooking into startup files
5 and practically limited to adding new shortcuts or removing all shortcuts bound
6 to a specific key. This release enables users to override existing terminal
7 shortcuts, disable them or add new keybindings.
8
9 For example, to set the :kbd:`right` to accept a single character of auto-suggestion
10 you could use::
11
12 my_shortcuts = [
13 {
14 "command": "IPython:auto_suggest.accept_character",
15 "new_keys": ["right"]
16 }
17 ]
18 %config TerminalInteractiveShell.shortcuts = my_shortcuts
19
20 You can learn more in :std:configtrait:`TerminalInteractiveShell.shortcuts`
21 configuration reference. No newline at end of file
@@ -1,808 +1,973 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 observe,
20 observe,
20 Instance,
21 Instance,
21 Type,
22 Type,
22 default,
23 default,
23 Enum,
24 Enum,
24 Union,
25 Union,
25 Any,
26 Any,
26 validate,
27 validate,
27 Float,
28 Float,
28 )
29 )
29
30
30 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
31 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
31 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
32 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
32 from prompt_toolkit.filters import (HasFocus, Condition, IsDone)
33 from prompt_toolkit.filters import HasFocus, Condition, IsDone
33 from prompt_toolkit.formatted_text import PygmentsTokens
34 from prompt_toolkit.formatted_text import PygmentsTokens
34 from prompt_toolkit.history import History
35 from prompt_toolkit.history import History
35 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
36 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
36 from prompt_toolkit.output import ColorDepth
37 from prompt_toolkit.output import ColorDepth
37 from prompt_toolkit.patch_stdout import patch_stdout
38 from prompt_toolkit.patch_stdout import patch_stdout
38 from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
39 from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
39 from prompt_toolkit.styles import DynamicStyle, merge_styles
40 from prompt_toolkit.styles import DynamicStyle, merge_styles
40 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
41 from prompt_toolkit import __version__ as ptk_version
42 from prompt_toolkit import __version__ as ptk_version
42
43
43 from pygments.styles import get_style_by_name
44 from pygments.styles import get_style_by_name
44 from pygments.style import Style
45 from pygments.style import Style
45 from pygments.token import Token
46 from pygments.token import Token
46
47
47 from .debugger import TerminalPdb, Pdb
48 from .debugger import TerminalPdb, Pdb
48 from .magics import TerminalMagics
49 from .magics import TerminalMagics
49 from .pt_inputhooks import get_inputhook_name_and_func
50 from .pt_inputhooks import get_inputhook_name_and_func
50 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
51 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
51 from .ptutils import IPythonPTCompleter, IPythonPTLexer
52 from .ptutils import IPythonPTCompleter, IPythonPTLexer
52 from .shortcuts import create_ipython_shortcuts
53 from .shortcuts import (
54 create_ipython_shortcuts,
55 create_identifier,
56 RuntimeBinding,
57 add_binding,
58 )
59 from .shortcuts.filters import KEYBINDING_FILTERS, filter_from_string
53 from .shortcuts.auto_suggest import (
60 from .shortcuts.auto_suggest import (
54 NavigableAutoSuggestFromHistory,
61 NavigableAutoSuggestFromHistory,
55 AppendAutoSuggestionInAnyLine,
62 AppendAutoSuggestionInAnyLine,
56 )
63 )
57
64
58 PTK3 = ptk_version.startswith('3.')
65 PTK3 = ptk_version.startswith('3.')
59
66
60
67
61 class _NoStyle(Style): pass
68 class _NoStyle(Style): pass
62
69
63
70
64
71
65 _style_overrides_light_bg = {
72 _style_overrides_light_bg = {
66 Token.Prompt: '#ansibrightblue',
73 Token.Prompt: '#ansibrightblue',
67 Token.PromptNum: '#ansiblue bold',
74 Token.PromptNum: '#ansiblue bold',
68 Token.OutPrompt: '#ansibrightred',
75 Token.OutPrompt: '#ansibrightred',
69 Token.OutPromptNum: '#ansired bold',
76 Token.OutPromptNum: '#ansired bold',
70 }
77 }
71
78
72 _style_overrides_linux = {
79 _style_overrides_linux = {
73 Token.Prompt: '#ansibrightgreen',
80 Token.Prompt: '#ansibrightgreen',
74 Token.PromptNum: '#ansigreen bold',
81 Token.PromptNum: '#ansigreen bold',
75 Token.OutPrompt: '#ansibrightred',
82 Token.OutPrompt: '#ansibrightred',
76 Token.OutPromptNum: '#ansired bold',
83 Token.OutPromptNum: '#ansired bold',
77 }
84 }
78
85
79 def get_default_editor():
86 def get_default_editor():
80 try:
87 try:
81 return os.environ['EDITOR']
88 return os.environ['EDITOR']
82 except KeyError:
89 except KeyError:
83 pass
90 pass
84 except UnicodeError:
91 except UnicodeError:
85 warn("$EDITOR environment variable is not pure ASCII. Using platform "
92 warn("$EDITOR environment variable is not pure ASCII. Using platform "
86 "default editor.")
93 "default editor.")
87
94
88 if os.name == 'posix':
95 if os.name == 'posix':
89 return 'vi' # the only one guaranteed to be there!
96 return 'vi' # the only one guaranteed to be there!
90 else:
97 else:
91 return 'notepad' # same in Windows!
98 return 'notepad' # same in Windows!
92
99
93 # conservatively check for tty
100 # conservatively check for tty
94 # overridden streams can result in things like:
101 # overridden streams can result in things like:
95 # - sys.stdin = None
102 # - sys.stdin = None
96 # - no isatty method
103 # - no isatty method
97 for _name in ('stdin', 'stdout', 'stderr'):
104 for _name in ('stdin', 'stdout', 'stderr'):
98 _stream = getattr(sys, _name)
105 _stream = getattr(sys, _name)
99 try:
106 try:
100 if not _stream or not hasattr(_stream, "isatty") or not _stream.isatty():
107 if not _stream or not hasattr(_stream, "isatty") or not _stream.isatty():
101 _is_tty = False
108 _is_tty = False
102 break
109 break
103 except ValueError:
110 except ValueError:
104 # stream is closed
111 # stream is closed
105 _is_tty = False
112 _is_tty = False
106 break
113 break
107 else:
114 else:
108 _is_tty = True
115 _is_tty = True
109
116
110
117
111 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
118 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
112
119
113 def black_reformat_handler(text_before_cursor):
120 def black_reformat_handler(text_before_cursor):
114 """
121 """
115 We do not need to protect against error,
122 We do not need to protect against error,
116 this is taken care at a higher level where any reformat error is ignored.
123 this is taken care at a higher level where any reformat error is ignored.
117 Indeed we may call reformatting on incomplete code.
124 Indeed we may call reformatting on incomplete code.
118 """
125 """
119 import black
126 import black
120
127
121 formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
128 formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
122 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
129 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
123 formatted_text = formatted_text[:-1]
130 formatted_text = formatted_text[:-1]
124 return formatted_text
131 return formatted_text
125
132
126
133
127 def yapf_reformat_handler(text_before_cursor):
134 def yapf_reformat_handler(text_before_cursor):
128 from yapf.yapflib import file_resources
135 from yapf.yapflib import file_resources
129 from yapf.yapflib import yapf_api
136 from yapf.yapflib import yapf_api
130
137
131 style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
138 style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
132 formatted_text, was_formatted = yapf_api.FormatCode(
139 formatted_text, was_formatted = yapf_api.FormatCode(
133 text_before_cursor, style_config=style_config
140 text_before_cursor, style_config=style_config
134 )
141 )
135 if was_formatted:
142 if was_formatted:
136 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
143 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
137 formatted_text = formatted_text[:-1]
144 formatted_text = formatted_text[:-1]
138 return formatted_text
145 return formatted_text
139 else:
146 else:
140 return text_before_cursor
147 return text_before_cursor
141
148
142
149
143 class PtkHistoryAdapter(History):
150 class PtkHistoryAdapter(History):
144 """
151 """
145 Prompt toolkit has it's own way of handling history, Where it assumes it can
152 Prompt toolkit has it's own way of handling history, Where it assumes it can
146 Push/pull from history.
153 Push/pull from history.
147
154
148 """
155 """
149
156
150 def __init__(self, shell):
157 def __init__(self, shell):
151 super().__init__()
158 super().__init__()
152 self.shell = shell
159 self.shell = shell
153 self._refresh()
160 self._refresh()
154
161
155 def append_string(self, string):
162 def append_string(self, string):
156 # we rely on sql for that.
163 # we rely on sql for that.
157 self._loaded = False
164 self._loaded = False
158 self._refresh()
165 self._refresh()
159
166
160 def _refresh(self):
167 def _refresh(self):
161 if not self._loaded:
168 if not self._loaded:
162 self._loaded_strings = list(self.load_history_strings())
169 self._loaded_strings = list(self.load_history_strings())
163
170
164 def load_history_strings(self):
171 def load_history_strings(self):
165 last_cell = ""
172 last_cell = ""
166 res = []
173 res = []
167 for __, ___, cell in self.shell.history_manager.get_tail(
174 for __, ___, cell in self.shell.history_manager.get_tail(
168 self.shell.history_load_length, include_latest=True
175 self.shell.history_load_length, include_latest=True
169 ):
176 ):
170 # Ignore blank lines and consecutive duplicates
177 # Ignore blank lines and consecutive duplicates
171 cell = cell.rstrip()
178 cell = cell.rstrip()
172 if cell and (cell != last_cell):
179 if cell and (cell != last_cell):
173 res.append(cell)
180 res.append(cell)
174 last_cell = cell
181 last_cell = cell
175 yield from res[::-1]
182 yield from res[::-1]
176
183
177 def store_string(self, string: str) -> None:
184 def store_string(self, string: str) -> None:
178 pass
185 pass
179
186
180 class TerminalInteractiveShell(InteractiveShell):
187 class TerminalInteractiveShell(InteractiveShell):
181 mime_renderers = Dict().tag(config=True)
188 mime_renderers = Dict().tag(config=True)
182
189
183 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
190 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
184 'to reserve for the tab completion menu, '
191 'to reserve for the tab completion menu, '
185 'search history, ...etc, the height of '
192 'search history, ...etc, the height of '
186 'these menus will at most this value. '
193 'these menus will at most this value. '
187 'Increase it is you prefer long and skinny '
194 'Increase it is you prefer long and skinny '
188 'menus, decrease for short and wide.'
195 'menus, decrease for short and wide.'
189 ).tag(config=True)
196 ).tag(config=True)
190
197
191 pt_app: UnionType[PromptSession, None] = None
198 pt_app: UnionType[PromptSession, None] = None
192 auto_suggest: UnionType[
199 auto_suggest: UnionType[
193 AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None
200 AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None
194 ] = None
201 ] = None
195 debugger_history = None
202 debugger_history = None
196
203
197 debugger_history_file = Unicode(
204 debugger_history_file = Unicode(
198 "~/.pdbhistory", help="File in which to store and read history"
205 "~/.pdbhistory", help="File in which to store and read history"
199 ).tag(config=True)
206 ).tag(config=True)
200
207
201 simple_prompt = Bool(_use_simple_prompt,
208 simple_prompt = Bool(_use_simple_prompt,
202 help="""Use `raw_input` for the REPL, without completion and prompt colors.
209 help="""Use `raw_input` for the REPL, without completion and prompt colors.
203
210
204 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
211 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
205 IPython own testing machinery, and emacs inferior-shell integration through elpy.
212 IPython own testing machinery, and emacs inferior-shell integration through elpy.
206
213
207 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
214 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
208 environment variable is set, or the current terminal is not a tty."""
215 environment variable is set, or the current terminal is not a tty."""
209 ).tag(config=True)
216 ).tag(config=True)
210
217
211 @property
218 @property
212 def debugger_cls(self):
219 def debugger_cls(self):
213 return Pdb if self.simple_prompt else TerminalPdb
220 return Pdb if self.simple_prompt else TerminalPdb
214
221
215 confirm_exit = Bool(True,
222 confirm_exit = Bool(True,
216 help="""
223 help="""
217 Set to confirm when you try to exit IPython with an EOF (Control-D
224 Set to confirm when you try to exit IPython with an EOF (Control-D
218 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
225 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
219 you can force a direct exit without any confirmation.""",
226 you can force a direct exit without any confirmation.""",
220 ).tag(config=True)
227 ).tag(config=True)
221
228
222 editing_mode = Unicode('emacs',
229 editing_mode = Unicode('emacs',
223 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
230 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
224 ).tag(config=True)
231 ).tag(config=True)
225
232
226 emacs_bindings_in_vi_insert_mode = Bool(
233 emacs_bindings_in_vi_insert_mode = Bool(
227 True,
234 True,
228 help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.",
235 help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.",
229 ).tag(config=True)
236 ).tag(config=True)
230
237
231 modal_cursor = Bool(
238 modal_cursor = Bool(
232 True,
239 True,
233 help="""
240 help="""
234 Cursor shape changes depending on vi mode: beam in vi insert mode,
241 Cursor shape changes depending on vi mode: beam in vi insert mode,
235 block in nav mode, underscore in replace mode.""",
242 block in nav mode, underscore in replace mode.""",
236 ).tag(config=True)
243 ).tag(config=True)
237
244
238 ttimeoutlen = Float(
245 ttimeoutlen = Float(
239 0.01,
246 0.01,
240 help="""The time in milliseconds that is waited for a key code
247 help="""The time in milliseconds that is waited for a key code
241 to complete.""",
248 to complete.""",
242 ).tag(config=True)
249 ).tag(config=True)
243
250
244 timeoutlen = Float(
251 timeoutlen = Float(
245 0.5,
252 0.5,
246 help="""The time in milliseconds that is waited for a mapped key
253 help="""The time in milliseconds that is waited for a mapped key
247 sequence to complete.""",
254 sequence to complete.""",
248 ).tag(config=True)
255 ).tag(config=True)
249
256
250 autoformatter = Unicode(
257 autoformatter = Unicode(
251 None,
258 None,
252 help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`",
259 help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`",
253 allow_none=True
260 allow_none=True
254 ).tag(config=True)
261 ).tag(config=True)
255
262
256 auto_match = Bool(
263 auto_match = Bool(
257 False,
264 False,
258 help="""
265 help="""
259 Automatically add/delete closing bracket or quote when opening bracket or quote is entered/deleted.
266 Automatically add/delete closing bracket or quote when opening bracket or quote is entered/deleted.
260 Brackets: (), [], {}
267 Brackets: (), [], {}
261 Quotes: '', \"\"
268 Quotes: '', \"\"
262 """,
269 """,
263 ).tag(config=True)
270 ).tag(config=True)
264
271
265 mouse_support = Bool(False,
272 mouse_support = Bool(False,
266 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
273 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
267 ).tag(config=True)
274 ).tag(config=True)
268
275
269 # We don't load the list of styles for the help string, because loading
276 # We don't load the list of styles for the help string, because loading
270 # Pygments plugins takes time and can cause unexpected errors.
277 # Pygments plugins takes time and can cause unexpected errors.
271 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
278 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
272 help="""The name or class of a Pygments style to use for syntax
279 help="""The name or class of a Pygments style to use for syntax
273 highlighting. To see available styles, run `pygmentize -L styles`."""
280 highlighting. To see available styles, run `pygmentize -L styles`."""
274 ).tag(config=True)
281 ).tag(config=True)
275
282
276 @validate('editing_mode')
283 @validate('editing_mode')
277 def _validate_editing_mode(self, proposal):
284 def _validate_editing_mode(self, proposal):
278 if proposal['value'].lower() == 'vim':
285 if proposal['value'].lower() == 'vim':
279 proposal['value']= 'vi'
286 proposal['value']= 'vi'
280 elif proposal['value'].lower() == 'default':
287 elif proposal['value'].lower() == 'default':
281 proposal['value']= 'emacs'
288 proposal['value']= 'emacs'
282
289
283 if hasattr(EditingMode, proposal['value'].upper()):
290 if hasattr(EditingMode, proposal['value'].upper()):
284 return proposal['value'].lower()
291 return proposal['value'].lower()
285
292
286 return self.editing_mode
293 return self.editing_mode
287
294
288
295
289 @observe('editing_mode')
296 @observe('editing_mode')
290 def _editing_mode(self, change):
297 def _editing_mode(self, change):
291 if self.pt_app:
298 if self.pt_app:
292 self.pt_app.editing_mode = getattr(EditingMode, change.new.upper())
299 self.pt_app.editing_mode = getattr(EditingMode, change.new.upper())
293
300
294 def _set_formatter(self, formatter):
301 def _set_formatter(self, formatter):
295 if formatter is None:
302 if formatter is None:
296 self.reformat_handler = lambda x:x
303 self.reformat_handler = lambda x:x
297 elif formatter == 'black':
304 elif formatter == 'black':
298 self.reformat_handler = black_reformat_handler
305 self.reformat_handler = black_reformat_handler
299 elif formatter == "yapf":
306 elif formatter == "yapf":
300 self.reformat_handler = yapf_reformat_handler
307 self.reformat_handler = yapf_reformat_handler
301 else:
308 else:
302 raise ValueError
309 raise ValueError
303
310
304 @observe("autoformatter")
311 @observe("autoformatter")
305 def _autoformatter_changed(self, change):
312 def _autoformatter_changed(self, change):
306 formatter = change.new
313 formatter = change.new
307 self._set_formatter(formatter)
314 self._set_formatter(formatter)
308
315
309 @observe('highlighting_style')
316 @observe('highlighting_style')
310 @observe('colors')
317 @observe('colors')
311 def _highlighting_style_changed(self, change):
318 def _highlighting_style_changed(self, change):
312 self.refresh_style()
319 self.refresh_style()
313
320
314 def refresh_style(self):
321 def refresh_style(self):
315 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
322 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
316
323
317
324
318 highlighting_style_overrides = Dict(
325 highlighting_style_overrides = Dict(
319 help="Override highlighting format for specific tokens"
326 help="Override highlighting format for specific tokens"
320 ).tag(config=True)
327 ).tag(config=True)
321
328
322 true_color = Bool(False,
329 true_color = Bool(False,
323 help="""Use 24bit colors instead of 256 colors in prompt highlighting.
330 help="""Use 24bit colors instead of 256 colors in prompt highlighting.
324 If your terminal supports true color, the following command should
331 If your terminal supports true color, the following command should
325 print ``TRUECOLOR`` in orange::
332 print ``TRUECOLOR`` in orange::
326
333
327 printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"
334 printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"
328 """,
335 """,
329 ).tag(config=True)
336 ).tag(config=True)
330
337
331 editor = Unicode(get_default_editor(),
338 editor = Unicode(get_default_editor(),
332 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
339 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
333 ).tag(config=True)
340 ).tag(config=True)
334
341
335 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
342 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
336
343
337 prompts = Instance(Prompts)
344 prompts = Instance(Prompts)
338
345
339 @default('prompts')
346 @default('prompts')
340 def _prompts_default(self):
347 def _prompts_default(self):
341 return self.prompts_class(self)
348 return self.prompts_class(self)
342
349
343 # @observe('prompts')
350 # @observe('prompts')
344 # def _(self, change):
351 # def _(self, change):
345 # self._update_layout()
352 # self._update_layout()
346
353
347 @default('displayhook_class')
354 @default('displayhook_class')
348 def _displayhook_class_default(self):
355 def _displayhook_class_default(self):
349 return RichPromptDisplayHook
356 return RichPromptDisplayHook
350
357
351 term_title = Bool(True,
358 term_title = Bool(True,
352 help="Automatically set the terminal title"
359 help="Automatically set the terminal title"
353 ).tag(config=True)
360 ).tag(config=True)
354
361
355 term_title_format = Unicode("IPython: {cwd}",
362 term_title_format = Unicode("IPython: {cwd}",
356 help="Customize the terminal title format. This is a python format string. " +
363 help="Customize the terminal title format. This is a python format string. " +
357 "Available substitutions are: {cwd}."
364 "Available substitutions are: {cwd}."
358 ).tag(config=True)
365 ).tag(config=True)
359
366
360 display_completions = Enum(('column', 'multicolumn','readlinelike'),
367 display_completions = Enum(('column', 'multicolumn','readlinelike'),
361 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
368 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
362 "'readlinelike'. These options are for `prompt_toolkit`, see "
369 "'readlinelike'. These options are for `prompt_toolkit`, see "
363 "`prompt_toolkit` documentation for more information."
370 "`prompt_toolkit` documentation for more information."
364 ),
371 ),
365 default_value='multicolumn').tag(config=True)
372 default_value='multicolumn').tag(config=True)
366
373
367 highlight_matching_brackets = Bool(True,
374 highlight_matching_brackets = Bool(True,
368 help="Highlight matching brackets.",
375 help="Highlight matching brackets.",
369 ).tag(config=True)
376 ).tag(config=True)
370
377
371 extra_open_editor_shortcuts = Bool(False,
378 extra_open_editor_shortcuts = Bool(False,
372 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
379 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
373 "This is in addition to the F2 binding, which is always enabled."
380 "This is in addition to the F2 binding, which is always enabled."
374 ).tag(config=True)
381 ).tag(config=True)
375
382
376 handle_return = Any(None,
383 handle_return = Any(None,
377 help="Provide an alternative handler to be called when the user presses "
384 help="Provide an alternative handler to be called when the user presses "
378 "Return. This is an advanced option intended for debugging, which "
385 "Return. This is an advanced option intended for debugging, which "
379 "may be changed or removed in later releases."
386 "may be changed or removed in later releases."
380 ).tag(config=True)
387 ).tag(config=True)
381
388
382 enable_history_search = Bool(True,
389 enable_history_search = Bool(True,
383 help="Allows to enable/disable the prompt toolkit history search"
390 help="Allows to enable/disable the prompt toolkit history search"
384 ).tag(config=True)
391 ).tag(config=True)
385
392
386 autosuggestions_provider = Unicode(
393 autosuggestions_provider = Unicode(
387 "NavigableAutoSuggestFromHistory",
394 "NavigableAutoSuggestFromHistory",
388 help="Specifies from which source automatic suggestions are provided. "
395 help="Specifies from which source automatic suggestions are provided. "
389 "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and "
396 "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and "
390 ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, "
397 ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, "
391 " or ``None`` to disable automatic suggestions. "
398 " or ``None`` to disable automatic suggestions. "
392 "Default is `'NavigableAutoSuggestFromHistory`'.",
399 "Default is `'NavigableAutoSuggestFromHistory`'.",
393 allow_none=True,
400 allow_none=True,
394 ).tag(config=True)
401 ).tag(config=True)
395
402
396 def _set_autosuggestions(self, provider):
403 def _set_autosuggestions(self, provider):
397 # disconnect old handler
404 # disconnect old handler
398 if self.auto_suggest and isinstance(
405 if self.auto_suggest and isinstance(
399 self.auto_suggest, NavigableAutoSuggestFromHistory
406 self.auto_suggest, NavigableAutoSuggestFromHistory
400 ):
407 ):
401 self.auto_suggest.disconnect()
408 self.auto_suggest.disconnect()
402 if provider is None:
409 if provider is None:
403 self.auto_suggest = None
410 self.auto_suggest = None
404 elif provider == "AutoSuggestFromHistory":
411 elif provider == "AutoSuggestFromHistory":
405 self.auto_suggest = AutoSuggestFromHistory()
412 self.auto_suggest = AutoSuggestFromHistory()
406 elif provider == "NavigableAutoSuggestFromHistory":
413 elif provider == "NavigableAutoSuggestFromHistory":
407 self.auto_suggest = NavigableAutoSuggestFromHistory()
414 self.auto_suggest = NavigableAutoSuggestFromHistory()
408 else:
415 else:
409 raise ValueError("No valid provider.")
416 raise ValueError("No valid provider.")
410 if self.pt_app:
417 if self.pt_app:
411 self.pt_app.auto_suggest = self.auto_suggest
418 self.pt_app.auto_suggest = self.auto_suggest
412
419
413 @observe("autosuggestions_provider")
420 @observe("autosuggestions_provider")
414 def _autosuggestions_provider_changed(self, change):
421 def _autosuggestions_provider_changed(self, change):
415 provider = change.new
422 provider = change.new
416 self._set_autosuggestions(provider)
423 self._set_autosuggestions(provider)
417
424
425 shortcuts = List(
426 trait=Dict(
427 key_trait=Enum(
428 [
429 "command",
430 "match_keys",
431 "match_filter",
432 "new_keys",
433 "new_filter",
434 "create",
435 ]
436 ),
437 per_key_traits={
438 "command": Unicode(),
439 "match_keys": List(Unicode()),
440 "match_filter": Unicode(),
441 "new_keys": List(Unicode()),
442 "new_filter": Unicode(),
443 "create": Bool(False),
444 },
445 ),
446 help="""Add, disable or modifying shortcuts.
447
448 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 one of the following:
451
452 - ``match_keys``: list of keys used to match an existing shortcut,
453 - ``match_filter``: shortcut filter used to match an existing shortcut,
454 - ``new_keys``: list of keys to set,
455 - ``new_filter``: a new shortcut filter to set
456
457 The filters have to be composed of pre-defined verbs and joined by one
458 of the following conjunctions: ``&`` (and), ``|`` (or), ``~`` (not).
459 The pre-defined verbs are:
460
461 {}
462
463
464 To disable a shortcut set ``new_keys`` to an empty list.
465 To add a shortcut add key ``create`` with value ``True``.
466
467 When modifying/disabling shortcuts, ``match_keys``/``match_filter`` can
468 be omitted if the provided specification uniquely identifies a shortcut
469 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 filter/keys.
472
473 Only shortcuts defined in IPython (and not default prompt-toolkit
474 shortcuts) can be modified or disabled. The full list of shortcuts,
475 command identifiers and filters is available under
476 :ref:`terminal-shortcuts-list`.
477 """.format(
478 "\n ".join([f"- `{k}`" for k in KEYBINDING_FILTERS])
479 ),
480 ).tag(config=True)
481
482 @observe("shortcuts")
483 def _shortcuts_changed(self, change):
484 if self.pt_app:
485 self.pt_app.key_bindings = self._merge_shortcuts(user_shortcuts=change.new)
486
487 def _merge_shortcuts(self, user_shortcuts):
488 # rebuild the bindings list from scratch
489 key_bindings = create_ipython_shortcuts(self)
490
491 # for now we only allow adding shortcuts for commands which are already
492 # registered; this is a security precaution.
493 known_commands = {
494 create_identifier(binding.handler): binding.handler
495 for binding in key_bindings.bindings
496 }
497 shortcuts_to_skip = []
498 shortcuts_to_add = []
499
500 for shortcut in user_shortcuts:
501 command_id = shortcut["command"]
502 if command_id not in known_commands:
503 allowed_commands = "\n - ".join(known_commands)
504 raise ValueError(
505 f"{command_id} is not a known shortcut command."
506 f" Allowed commands are: \n - {allowed_commands}"
507 )
508 old_keys = shortcut.get("match_keys", None)
509 old_filter = (
510 filter_from_string(shortcut["match_filter"])
511 if "match_filter" in shortcut
512 else None
513 )
514 matching = [
515 binding
516 for binding in key_bindings.bindings
517 if (
518 (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 create_identifier(binding.handler) == command_id
521 )
522 ]
523
524 new_keys = shortcut.get("new_keys", None)
525 new_filter = shortcut.get("new_filter", None)
526
527 command = known_commands[command_id]
528
529 creating_new = shortcut.get("create", False)
530 modifying_existing = not creating_new and (
531 new_keys is not None or new_filter
532 )
533
534 if creating_new and new_keys == []:
535 raise ValueError("Cannot add a shortcut without keys")
536
537 if modifying_existing:
538 specification = {
539 key: shortcut[key]
540 for key in ["command", "filter"]
541 if key in shortcut
542 }
543 if len(matching) == 0:
544 raise ValueError(f"No shortcuts matching {specification} found")
545 elif len(matching) > 1:
546 raise ValueError(
547 f"Multiple shortcuts matching {specification} found,"
548 f" please add keys/filter to select one of: {matching}"
549 )
550
551 matched = matching[0]
552 old_filter = matched.filter
553 old_keys = list(matched.keys)
554 shortcuts_to_skip.append(
555 RuntimeBinding(
556 command,
557 keys=old_keys,
558 filter=old_filter,
559 )
560 )
561
562 if new_keys != []:
563 shortcuts_to_add.append(
564 RuntimeBinding(
565 command,
566 keys=new_keys or old_keys,
567 filter=filter_from_string(new_filter)
568 if new_filter is not None
569 else (
570 old_filter
571 if old_filter is not None
572 else filter_from_string("always")
573 ),
574 )
575 )
576
577 # rebuild the bindings list from scratch
578 key_bindings = create_ipython_shortcuts(self, skip=shortcuts_to_skip)
579 for binding in shortcuts_to_add:
580 add_binding(key_bindings, binding)
581
582 return key_bindings
583
418 prompt_includes_vi_mode = Bool(True,
584 prompt_includes_vi_mode = Bool(True,
419 help="Display the current vi mode (when using vi editing mode)."
585 help="Display the current vi mode (when using vi editing mode)."
420 ).tag(config=True)
586 ).tag(config=True)
421
587
422 @observe('term_title')
588 @observe('term_title')
423 def init_term_title(self, change=None):
589 def init_term_title(self, change=None):
424 # Enable or disable the terminal title.
590 # Enable or disable the terminal title.
425 if self.term_title and _is_tty:
591 if self.term_title and _is_tty:
426 toggle_set_term_title(True)
592 toggle_set_term_title(True)
427 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
593 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
428 else:
594 else:
429 toggle_set_term_title(False)
595 toggle_set_term_title(False)
430
596
431 def restore_term_title(self):
597 def restore_term_title(self):
432 if self.term_title and _is_tty:
598 if self.term_title and _is_tty:
433 restore_term_title()
599 restore_term_title()
434
600
435 def init_display_formatter(self):
601 def init_display_formatter(self):
436 super(TerminalInteractiveShell, self).init_display_formatter()
602 super(TerminalInteractiveShell, self).init_display_formatter()
437 # terminal only supports plain text
603 # terminal only supports plain text
438 self.display_formatter.active_types = ["text/plain"]
604 self.display_formatter.active_types = ["text/plain"]
439
605
440 def init_prompt_toolkit_cli(self):
606 def init_prompt_toolkit_cli(self):
441 if self.simple_prompt:
607 if self.simple_prompt:
442 # Fall back to plain non-interactive output for tests.
608 # Fall back to plain non-interactive output for tests.
443 # This is very limited.
609 # This is very limited.
444 def prompt():
610 def prompt():
445 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
611 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
446 lines = [input(prompt_text)]
612 lines = [input(prompt_text)]
447 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
613 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
448 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
614 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
449 lines.append( input(prompt_continuation) )
615 lines.append( input(prompt_continuation) )
450 return '\n'.join(lines)
616 return '\n'.join(lines)
451 self.prompt_for_code = prompt
617 self.prompt_for_code = prompt
452 return
618 return
453
619
454 # Set up keyboard shortcuts
620 # Set up keyboard shortcuts
455 key_bindings = create_ipython_shortcuts(self)
621 key_bindings = self._merge_shortcuts(user_shortcuts=self.shortcuts)
456
457
622
458 # Pre-populate history from IPython's history database
623 # Pre-populate history from IPython's history database
459 history = PtkHistoryAdapter(self)
624 history = PtkHistoryAdapter(self)
460
625
461 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
626 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
462 self.style = DynamicStyle(lambda: self._style)
627 self.style = DynamicStyle(lambda: self._style)
463
628
464 editing_mode = getattr(EditingMode, self.editing_mode.upper())
629 editing_mode = getattr(EditingMode, self.editing_mode.upper())
465
630
466 self.pt_loop = asyncio.new_event_loop()
631 self.pt_loop = asyncio.new_event_loop()
467 self.pt_app = PromptSession(
632 self.pt_app = PromptSession(
468 auto_suggest=self.auto_suggest,
633 auto_suggest=self.auto_suggest,
469 editing_mode=editing_mode,
634 editing_mode=editing_mode,
470 key_bindings=key_bindings,
635 key_bindings=key_bindings,
471 history=history,
636 history=history,
472 completer=IPythonPTCompleter(shell=self),
637 completer=IPythonPTCompleter(shell=self),
473 enable_history_search=self.enable_history_search,
638 enable_history_search=self.enable_history_search,
474 style=self.style,
639 style=self.style,
475 include_default_pygments_style=False,
640 include_default_pygments_style=False,
476 mouse_support=self.mouse_support,
641 mouse_support=self.mouse_support,
477 enable_open_in_editor=self.extra_open_editor_shortcuts,
642 enable_open_in_editor=self.extra_open_editor_shortcuts,
478 color_depth=self.color_depth,
643 color_depth=self.color_depth,
479 tempfile_suffix=".py",
644 tempfile_suffix=".py",
480 **self._extra_prompt_options()
645 **self._extra_prompt_options(),
481 )
646 )
482 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
647 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
483 self.auto_suggest.connect(self.pt_app)
648 self.auto_suggest.connect(self.pt_app)
484
649
485 def _make_style_from_name_or_cls(self, name_or_cls):
650 def _make_style_from_name_or_cls(self, name_or_cls):
486 """
651 """
487 Small wrapper that make an IPython compatible style from a style name
652 Small wrapper that make an IPython compatible style from a style name
488
653
489 We need that to add style for prompt ... etc.
654 We need that to add style for prompt ... etc.
490 """
655 """
491 style_overrides = {}
656 style_overrides = {}
492 if name_or_cls == 'legacy':
657 if name_or_cls == 'legacy':
493 legacy = self.colors.lower()
658 legacy = self.colors.lower()
494 if legacy == 'linux':
659 if legacy == 'linux':
495 style_cls = get_style_by_name('monokai')
660 style_cls = get_style_by_name('monokai')
496 style_overrides = _style_overrides_linux
661 style_overrides = _style_overrides_linux
497 elif legacy == 'lightbg':
662 elif legacy == 'lightbg':
498 style_overrides = _style_overrides_light_bg
663 style_overrides = _style_overrides_light_bg
499 style_cls = get_style_by_name('pastie')
664 style_cls = get_style_by_name('pastie')
500 elif legacy == 'neutral':
665 elif legacy == 'neutral':
501 # The default theme needs to be visible on both a dark background
666 # The default theme needs to be visible on both a dark background
502 # and a light background, because we can't tell what the terminal
667 # and a light background, because we can't tell what the terminal
503 # looks like. These tweaks to the default theme help with that.
668 # looks like. These tweaks to the default theme help with that.
504 style_cls = get_style_by_name('default')
669 style_cls = get_style_by_name('default')
505 style_overrides.update({
670 style_overrides.update({
506 Token.Number: '#ansigreen',
671 Token.Number: '#ansigreen',
507 Token.Operator: 'noinherit',
672 Token.Operator: 'noinherit',
508 Token.String: '#ansiyellow',
673 Token.String: '#ansiyellow',
509 Token.Name.Function: '#ansiblue',
674 Token.Name.Function: '#ansiblue',
510 Token.Name.Class: 'bold #ansiblue',
675 Token.Name.Class: 'bold #ansiblue',
511 Token.Name.Namespace: 'bold #ansiblue',
676 Token.Name.Namespace: 'bold #ansiblue',
512 Token.Name.Variable.Magic: '#ansiblue',
677 Token.Name.Variable.Magic: '#ansiblue',
513 Token.Prompt: '#ansigreen',
678 Token.Prompt: '#ansigreen',
514 Token.PromptNum: '#ansibrightgreen bold',
679 Token.PromptNum: '#ansibrightgreen bold',
515 Token.OutPrompt: '#ansired',
680 Token.OutPrompt: '#ansired',
516 Token.OutPromptNum: '#ansibrightred bold',
681 Token.OutPromptNum: '#ansibrightred bold',
517 })
682 })
518
683
519 # Hack: Due to limited color support on the Windows console
684 # Hack: Due to limited color support on the Windows console
520 # the prompt colors will be wrong without this
685 # the prompt colors will be wrong without this
521 if os.name == 'nt':
686 if os.name == 'nt':
522 style_overrides.update({
687 style_overrides.update({
523 Token.Prompt: '#ansidarkgreen',
688 Token.Prompt: '#ansidarkgreen',
524 Token.PromptNum: '#ansigreen bold',
689 Token.PromptNum: '#ansigreen bold',
525 Token.OutPrompt: '#ansidarkred',
690 Token.OutPrompt: '#ansidarkred',
526 Token.OutPromptNum: '#ansired bold',
691 Token.OutPromptNum: '#ansired bold',
527 })
692 })
528 elif legacy =='nocolor':
693 elif legacy =='nocolor':
529 style_cls=_NoStyle
694 style_cls=_NoStyle
530 style_overrides = {}
695 style_overrides = {}
531 else :
696 else :
532 raise ValueError('Got unknown colors: ', legacy)
697 raise ValueError('Got unknown colors: ', legacy)
533 else :
698 else :
534 if isinstance(name_or_cls, str):
699 if isinstance(name_or_cls, str):
535 style_cls = get_style_by_name(name_or_cls)
700 style_cls = get_style_by_name(name_or_cls)
536 else:
701 else:
537 style_cls = name_or_cls
702 style_cls = name_or_cls
538 style_overrides = {
703 style_overrides = {
539 Token.Prompt: '#ansigreen',
704 Token.Prompt: '#ansigreen',
540 Token.PromptNum: '#ansibrightgreen bold',
705 Token.PromptNum: '#ansibrightgreen bold',
541 Token.OutPrompt: '#ansired',
706 Token.OutPrompt: '#ansired',
542 Token.OutPromptNum: '#ansibrightred bold',
707 Token.OutPromptNum: '#ansibrightred bold',
543 }
708 }
544 style_overrides.update(self.highlighting_style_overrides)
709 style_overrides.update(self.highlighting_style_overrides)
545 style = merge_styles([
710 style = merge_styles([
546 style_from_pygments_cls(style_cls),
711 style_from_pygments_cls(style_cls),
547 style_from_pygments_dict(style_overrides),
712 style_from_pygments_dict(style_overrides),
548 ])
713 ])
549
714
550 return style
715 return style
551
716
552 @property
717 @property
553 def pt_complete_style(self):
718 def pt_complete_style(self):
554 return {
719 return {
555 'multicolumn': CompleteStyle.MULTI_COLUMN,
720 'multicolumn': CompleteStyle.MULTI_COLUMN,
556 'column': CompleteStyle.COLUMN,
721 'column': CompleteStyle.COLUMN,
557 'readlinelike': CompleteStyle.READLINE_LIKE,
722 'readlinelike': CompleteStyle.READLINE_LIKE,
558 }[self.display_completions]
723 }[self.display_completions]
559
724
560 @property
725 @property
561 def color_depth(self):
726 def color_depth(self):
562 return (ColorDepth.TRUE_COLOR if self.true_color else None)
727 return (ColorDepth.TRUE_COLOR if self.true_color else None)
563
728
564 def _extra_prompt_options(self):
729 def _extra_prompt_options(self):
565 """
730 """
566 Return the current layout option for the current Terminal InteractiveShell
731 Return the current layout option for the current Terminal InteractiveShell
567 """
732 """
568 def get_message():
733 def get_message():
569 return PygmentsTokens(self.prompts.in_prompt_tokens())
734 return PygmentsTokens(self.prompts.in_prompt_tokens())
570
735
571 if self.editing_mode == 'emacs':
736 if self.editing_mode == 'emacs':
572 # with emacs mode the prompt is (usually) static, so we call only
737 # with emacs mode the prompt is (usually) static, so we call only
573 # the function once. With VI mode it can toggle between [ins] and
738 # the function once. With VI mode it can toggle between [ins] and
574 # [nor] so we can't precompute.
739 # [nor] so we can't precompute.
575 # here I'm going to favor the default keybinding which almost
740 # here I'm going to favor the default keybinding which almost
576 # everybody uses to decrease CPU usage.
741 # everybody uses to decrease CPU usage.
577 # if we have issues with users with custom Prompts we can see how to
742 # if we have issues with users with custom Prompts we can see how to
578 # work around this.
743 # work around this.
579 get_message = get_message()
744 get_message = get_message()
580
745
581 options = {
746 options = {
582 "complete_in_thread": False,
747 "complete_in_thread": False,
583 "lexer": IPythonPTLexer(),
748 "lexer": IPythonPTLexer(),
584 "reserve_space_for_menu": self.space_for_menu,
749 "reserve_space_for_menu": self.space_for_menu,
585 "message": get_message,
750 "message": get_message,
586 "prompt_continuation": (
751 "prompt_continuation": (
587 lambda width, lineno, is_soft_wrap: PygmentsTokens(
752 lambda width, lineno, is_soft_wrap: PygmentsTokens(
588 self.prompts.continuation_prompt_tokens(width)
753 self.prompts.continuation_prompt_tokens(width)
589 )
754 )
590 ),
755 ),
591 "multiline": True,
756 "multiline": True,
592 "complete_style": self.pt_complete_style,
757 "complete_style": self.pt_complete_style,
593 "input_processors": [
758 "input_processors": [
594 # Highlight matching brackets, but only when this setting is
759 # Highlight matching brackets, but only when this setting is
595 # enabled, and only when the DEFAULT_BUFFER has the focus.
760 # enabled, and only when the DEFAULT_BUFFER has the focus.
596 ConditionalProcessor(
761 ConditionalProcessor(
597 processor=HighlightMatchingBracketProcessor(chars="[](){}"),
762 processor=HighlightMatchingBracketProcessor(chars="[](){}"),
598 filter=HasFocus(DEFAULT_BUFFER)
763 filter=HasFocus(DEFAULT_BUFFER)
599 & ~IsDone()
764 & ~IsDone()
600 & Condition(lambda: self.highlight_matching_brackets),
765 & Condition(lambda: self.highlight_matching_brackets),
601 ),
766 ),
602 # Show auto-suggestion in lines other than the last line.
767 # Show auto-suggestion in lines other than the last line.
603 ConditionalProcessor(
768 ConditionalProcessor(
604 processor=AppendAutoSuggestionInAnyLine(),
769 processor=AppendAutoSuggestionInAnyLine(),
605 filter=HasFocus(DEFAULT_BUFFER)
770 filter=HasFocus(DEFAULT_BUFFER)
606 & ~IsDone()
771 & ~IsDone()
607 & Condition(
772 & Condition(
608 lambda: isinstance(
773 lambda: isinstance(
609 self.auto_suggest, NavigableAutoSuggestFromHistory
774 self.auto_suggest, NavigableAutoSuggestFromHistory
610 )
775 )
611 ),
776 ),
612 ),
777 ),
613 ],
778 ],
614 }
779 }
615 if not PTK3:
780 if not PTK3:
616 options['inputhook'] = self.inputhook
781 options['inputhook'] = self.inputhook
617
782
618 return options
783 return options
619
784
620 def prompt_for_code(self):
785 def prompt_for_code(self):
621 if self.rl_next_input:
786 if self.rl_next_input:
622 default = self.rl_next_input
787 default = self.rl_next_input
623 self.rl_next_input = None
788 self.rl_next_input = None
624 else:
789 else:
625 default = ''
790 default = ''
626
791
627 # In order to make sure that asyncio code written in the
792 # In order to make sure that asyncio code written in the
628 # interactive shell doesn't interfere with the prompt, we run the
793 # interactive shell doesn't interfere with the prompt, we run the
629 # prompt in a different event loop.
794 # prompt in a different event loop.
630 # If we don't do this, people could spawn coroutine with a
795 # If we don't do this, people could spawn coroutine with a
631 # while/true inside which will freeze the prompt.
796 # while/true inside which will freeze the prompt.
632
797
633 policy = asyncio.get_event_loop_policy()
798 policy = asyncio.get_event_loop_policy()
634 old_loop = get_asyncio_loop()
799 old_loop = get_asyncio_loop()
635
800
636 # FIXME: prompt_toolkit is using the deprecated `asyncio.get_event_loop`
801 # FIXME: prompt_toolkit is using the deprecated `asyncio.get_event_loop`
637 # to get the current event loop.
802 # to get the current event loop.
638 # This will probably be replaced by an attribute or input argument,
803 # This will probably be replaced by an attribute or input argument,
639 # at which point we can stop calling the soon-to-be-deprecated `set_event_loop` here.
804 # at which point we can stop calling the soon-to-be-deprecated `set_event_loop` here.
640 if old_loop is not self.pt_loop:
805 if old_loop is not self.pt_loop:
641 policy.set_event_loop(self.pt_loop)
806 policy.set_event_loop(self.pt_loop)
642 try:
807 try:
643 with patch_stdout(raw=True):
808 with patch_stdout(raw=True):
644 text = self.pt_app.prompt(
809 text = self.pt_app.prompt(
645 default=default,
810 default=default,
646 **self._extra_prompt_options())
811 **self._extra_prompt_options())
647 finally:
812 finally:
648 # Restore the original event loop.
813 # Restore the original event loop.
649 if old_loop is not None and old_loop is not self.pt_loop:
814 if old_loop is not None and old_loop is not self.pt_loop:
650 policy.set_event_loop(old_loop)
815 policy.set_event_loop(old_loop)
651
816
652 return text
817 return text
653
818
654 def enable_win_unicode_console(self):
819 def enable_win_unicode_console(self):
655 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
820 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
656 # console by default, so WUC shouldn't be needed.
821 # console by default, so WUC shouldn't be needed.
657 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
822 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
658 DeprecationWarning,
823 DeprecationWarning,
659 stacklevel=2)
824 stacklevel=2)
660
825
661 def init_io(self):
826 def init_io(self):
662 if sys.platform not in {'win32', 'cli'}:
827 if sys.platform not in {'win32', 'cli'}:
663 return
828 return
664
829
665 import colorama
830 import colorama
666 colorama.init()
831 colorama.init()
667
832
668 def init_magics(self):
833 def init_magics(self):
669 super(TerminalInteractiveShell, self).init_magics()
834 super(TerminalInteractiveShell, self).init_magics()
670 self.register_magics(TerminalMagics)
835 self.register_magics(TerminalMagics)
671
836
672 def init_alias(self):
837 def init_alias(self):
673 # The parent class defines aliases that can be safely used with any
838 # The parent class defines aliases that can be safely used with any
674 # frontend.
839 # frontend.
675 super(TerminalInteractiveShell, self).init_alias()
840 super(TerminalInteractiveShell, self).init_alias()
676
841
677 # Now define aliases that only make sense on the terminal, because they
842 # Now define aliases that only make sense on the terminal, because they
678 # need direct access to the console in a way that we can't emulate in
843 # need direct access to the console in a way that we can't emulate in
679 # GUI or web frontend
844 # GUI or web frontend
680 if os.name == 'posix':
845 if os.name == 'posix':
681 for cmd in ('clear', 'more', 'less', 'man'):
846 for cmd in ('clear', 'more', 'less', 'man'):
682 self.alias_manager.soft_define_alias(cmd, cmd)
847 self.alias_manager.soft_define_alias(cmd, cmd)
683
848
684
849
685 def __init__(self, *args, **kwargs) -> None:
850 def __init__(self, *args, **kwargs) -> None:
686 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
851 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
687 self._set_autosuggestions(self.autosuggestions_provider)
852 self._set_autosuggestions(self.autosuggestions_provider)
688 self.init_prompt_toolkit_cli()
853 self.init_prompt_toolkit_cli()
689 self.init_term_title()
854 self.init_term_title()
690 self.keep_running = True
855 self.keep_running = True
691 self._set_formatter(self.autoformatter)
856 self._set_formatter(self.autoformatter)
692
857
693
858
694 def ask_exit(self):
859 def ask_exit(self):
695 self.keep_running = False
860 self.keep_running = False
696
861
697 rl_next_input = None
862 rl_next_input = None
698
863
699 def interact(self):
864 def interact(self):
700 self.keep_running = True
865 self.keep_running = True
701 while self.keep_running:
866 while self.keep_running:
702 print(self.separate_in, end='')
867 print(self.separate_in, end='')
703
868
704 try:
869 try:
705 code = self.prompt_for_code()
870 code = self.prompt_for_code()
706 except EOFError:
871 except EOFError:
707 if (not self.confirm_exit) \
872 if (not self.confirm_exit) \
708 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
873 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
709 self.ask_exit()
874 self.ask_exit()
710
875
711 else:
876 else:
712 if code:
877 if code:
713 self.run_cell(code, store_history=True)
878 self.run_cell(code, store_history=True)
714
879
715 def mainloop(self):
880 def mainloop(self):
716 # An extra layer of protection in case someone mashing Ctrl-C breaks
881 # An extra layer of protection in case someone mashing Ctrl-C breaks
717 # out of our internal code.
882 # out of our internal code.
718 while True:
883 while True:
719 try:
884 try:
720 self.interact()
885 self.interact()
721 break
886 break
722 except KeyboardInterrupt as e:
887 except KeyboardInterrupt as e:
723 print("\n%s escaped interact()\n" % type(e).__name__)
888 print("\n%s escaped interact()\n" % type(e).__name__)
724 finally:
889 finally:
725 # An interrupt during the eventloop will mess up the
890 # An interrupt during the eventloop will mess up the
726 # internal state of the prompt_toolkit library.
891 # internal state of the prompt_toolkit library.
727 # Stopping the eventloop fixes this, see
892 # Stopping the eventloop fixes this, see
728 # https://github.com/ipython/ipython/pull/9867
893 # https://github.com/ipython/ipython/pull/9867
729 if hasattr(self, '_eventloop'):
894 if hasattr(self, '_eventloop'):
730 self._eventloop.stop()
895 self._eventloop.stop()
731
896
732 self.restore_term_title()
897 self.restore_term_title()
733
898
734 # try to call some at-exit operation optimistically as some things can't
899 # try to call some at-exit operation optimistically as some things can't
735 # be done during interpreter shutdown. this is technically inaccurate as
900 # be done during interpreter shutdown. this is technically inaccurate as
736 # this make mainlool not re-callable, but that should be a rare if not
901 # this make mainlool not re-callable, but that should be a rare if not
737 # in existent use case.
902 # in existent use case.
738
903
739 self._atexit_once()
904 self._atexit_once()
740
905
741
906
742 _inputhook = None
907 _inputhook = None
743 def inputhook(self, context):
908 def inputhook(self, context):
744 if self._inputhook is not None:
909 if self._inputhook is not None:
745 self._inputhook(context)
910 self._inputhook(context)
746
911
747 active_eventloop = None
912 active_eventloop = None
748 def enable_gui(self, gui=None):
913 def enable_gui(self, gui=None):
749 if gui and (gui not in {"inline", "webagg"}):
914 if gui and (gui not in {"inline", "webagg"}):
750 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
915 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
751 else:
916 else:
752 self.active_eventloop = self._inputhook = None
917 self.active_eventloop = self._inputhook = None
753
918
754 # For prompt_toolkit 3.0. We have to create an asyncio event loop with
919 # For prompt_toolkit 3.0. We have to create an asyncio event loop with
755 # this inputhook.
920 # this inputhook.
756 if PTK3:
921 if PTK3:
757 import asyncio
922 import asyncio
758 from prompt_toolkit.eventloop import new_eventloop_with_inputhook
923 from prompt_toolkit.eventloop import new_eventloop_with_inputhook
759
924
760 if gui == 'asyncio':
925 if gui == 'asyncio':
761 # When we integrate the asyncio event loop, run the UI in the
926 # When we integrate the asyncio event loop, run the UI in the
762 # same event loop as the rest of the code. don't use an actual
927 # same event loop as the rest of the code. don't use an actual
763 # input hook. (Asyncio is not made for nesting event loops.)
928 # input hook. (Asyncio is not made for nesting event loops.)
764 self.pt_loop = get_asyncio_loop()
929 self.pt_loop = get_asyncio_loop()
765
930
766 elif self._inputhook:
931 elif self._inputhook:
767 # If an inputhook was set, create a new asyncio event loop with
932 # If an inputhook was set, create a new asyncio event loop with
768 # this inputhook for the prompt.
933 # this inputhook for the prompt.
769 self.pt_loop = new_eventloop_with_inputhook(self._inputhook)
934 self.pt_loop = new_eventloop_with_inputhook(self._inputhook)
770 else:
935 else:
771 # When there's no inputhook, run the prompt in a separate
936 # When there's no inputhook, run the prompt in a separate
772 # asyncio event loop.
937 # asyncio event loop.
773 self.pt_loop = asyncio.new_event_loop()
938 self.pt_loop = asyncio.new_event_loop()
774
939
775 # Run !system commands directly, not through pipes, so terminal programs
940 # Run !system commands directly, not through pipes, so terminal programs
776 # work correctly.
941 # work correctly.
777 system = InteractiveShell.system_raw
942 system = InteractiveShell.system_raw
778
943
779 def auto_rewrite_input(self, cmd):
944 def auto_rewrite_input(self, cmd):
780 """Overridden from the parent class to use fancy rewriting prompt"""
945 """Overridden from the parent class to use fancy rewriting prompt"""
781 if not self.show_rewritten_input:
946 if not self.show_rewritten_input:
782 return
947 return
783
948
784 tokens = self.prompts.rewrite_prompt_tokens()
949 tokens = self.prompts.rewrite_prompt_tokens()
785 if self.pt_app:
950 if self.pt_app:
786 print_formatted_text(PygmentsTokens(tokens), end='',
951 print_formatted_text(PygmentsTokens(tokens), end='',
787 style=self.pt_app.app.style)
952 style=self.pt_app.app.style)
788 print(cmd)
953 print(cmd)
789 else:
954 else:
790 prompt = ''.join(s for t, s in tokens)
955 prompt = ''.join(s for t, s in tokens)
791 print(prompt, cmd, sep='')
956 print(prompt, cmd, sep='')
792
957
793 _prompts_before = None
958 _prompts_before = None
794 def switch_doctest_mode(self, mode):
959 def switch_doctest_mode(self, mode):
795 """Switch prompts to classic for %doctest_mode"""
960 """Switch prompts to classic for %doctest_mode"""
796 if mode:
961 if mode:
797 self._prompts_before = self.prompts
962 self._prompts_before = self.prompts
798 self.prompts = ClassicPrompts(self)
963 self.prompts = ClassicPrompts(self)
799 elif self._prompts_before:
964 elif self._prompts_before:
800 self.prompts = self._prompts_before
965 self.prompts = self._prompts_before
801 self._prompts_before = None
966 self._prompts_before = None
802 # self._update_layout()
967 # self._update_layout()
803
968
804
969
805 InteractiveShellABC.register(TerminalInteractiveShell)
970 InteractiveShellABC.register(TerminalInteractiveShell)
806
971
807 if __name__ == '__main__':
972 if __name__ == '__main__':
808 TerminalInteractiveShell.instance().interact()
973 TerminalInteractiveShell.instance().interact()
This diff has been collapsed as it changes many lines, (742 lines changed) Show them Hide them
@@ -1,670 +1,626 b''
1 """
1 """
2 Module to define and register Terminal IPython shortcuts with
2 Module to define and register Terminal IPython shortcuts with
3 :mod:`prompt_toolkit`
3 :mod:`prompt_toolkit`
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 import os
9 import os
10 import re
11 import signal
10 import signal
12 import sys
11 import sys
13 import warnings
12 import warnings
14 from typing import Callable, Dict, Union
13 from dataclasses import dataclass
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.enums import DEFAULT_BUFFER, SEARCH_BUFFER
18 from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions
19 from prompt_toolkit.filters import has_focus as has_focus_impl
20 from prompt_toolkit.filters import (
21 has_selection,
22 has_suggestion,
23 vi_insert_mode,
24 vi_mode,
25 )
26 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
27 from prompt_toolkit.key_binding.bindings import named_commands as nc
19 from prompt_toolkit.key_binding.bindings import named_commands as nc
28 from prompt_toolkit.key_binding.bindings.completion import (
20 from prompt_toolkit.key_binding.bindings.completion import (
29 display_completions_like_readline,
21 display_completions_like_readline,
30 )
22 )
31 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
23 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
32 from prompt_toolkit.layout.layout import FocusableElement
24 from prompt_toolkit.filters import Condition
33
25
26 from IPython.core.getipython import get_ipython
34 from IPython.terminal.shortcuts import auto_match as match
27 from IPython.terminal.shortcuts import auto_match as match
35 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
36 from IPython.utils.decorators import undoc
30 from IPython.utils.decorators import undoc
37
31
38 __all__ = ["create_ipython_shortcuts"]
32 __all__ = ["create_ipython_shortcuts"]
39
33
40
34
41 @undoc
35 @dataclass
42 @Condition
36 class BaseBinding:
43 def cursor_in_leading_ws():
37 command: Callable[[KeyPressEvent], Any]
44 before = get_app().current_buffer.document.current_line_before_cursor
38 keys: List[str]
45 return (not before) or before.isspace()
46
47
48 def has_focus(value: FocusableElement):
49 """Wrapper around has_focus adding a nice `__name__` to tester function"""
50 tester = has_focus_impl(value).func
51 tester.__name__ = f"is_focused({value})"
52 return Condition(tester)
53
54
55 @undoc
56 @Condition
57 def has_line_below() -> bool:
58 document = get_app().current_buffer.document
59 return document.cursor_position_row < len(document.lines) - 1
60
61
62 @undoc
63 @Condition
64 def has_line_above() -> bool:
65 document = get_app().current_buffer.document
66 return document.cursor_position_row != 0
67
68
69 def create_ipython_shortcuts(shell, for_all_platforms: bool = False) -> KeyBindings:
70 """Set up the prompt_toolkit keyboard shortcuts for IPython.
71
72 Parameters
73 ----------
74 shell: InteractiveShell
75 The current IPython shell Instance
76 for_all_platforms: bool (default false)
77 This parameter is mostly used in generating the documentation
78 to create the shortcut binding for all the platforms, and export
79 them.
80
81 Returns
82 -------
83 KeyBindings
84 the keybinding instance for prompt toolkit.
85
39
86 """
87 # Warning: if possible, do NOT define handler functions in the locals
88 # scope of this function, instead define functions in the global
89 # scope, or a separate module, and include a user-friendly docstring
90 # describing the action.
91
40
92 kb = KeyBindings()
41 @dataclass
93 insert_mode = vi_insert_mode | emacs_insert_mode
42 class RuntimeBinding(BaseBinding):
43 filter: Condition
94
44
95 if getattr(shell, "handle_return", None):
96 return_handler = shell.handle_return(shell)
97 else:
98 return_handler = newline_or_execute_outer(shell)
99
45
100 kb.add("enter", filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode))(
46 @dataclass
101 return_handler
47 class Binding(BaseBinding):
102 )
48 # while filter could be created by referencing variables directly (rather
103
49 # than created from strings), by using strings we ensure that users will
104 @Condition
50 # be able to create filters in configuration (e.g. JSON) files too, which
105 def ebivim():
51 # also benefits the documentation by enforcing human-readable filter names.
106 return shell.emacs_bindings_in_vi_insert_mode
52 condition: Optional[str] = None
107
108 @kb.add(
109 "escape",
110 "enter",
111 filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode & ebivim),
112 )
113 def reformat_and_execute(event):
114 """Reformat code and execute it"""
115 reformat_text_before_cursor(
116 event.current_buffer, event.current_buffer.document, shell
117 )
118 event.current_buffer.validate_and_handle()
119
120 kb.add("c-\\")(quit)
121
122 kb.add("c-p", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))(
123 previous_history_or_previous_completion
124 )
125
126 kb.add("c-n", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))(
127 next_history_or_next_completion
128 )
129
130 kb.add("c-g", filter=(has_focus(DEFAULT_BUFFER) & has_completions))(
131 dismiss_completion
132 )
133
134 kb.add("c-c", filter=has_focus(DEFAULT_BUFFER))(reset_buffer)
135
136 kb.add("c-c", filter=has_focus(SEARCH_BUFFER))(reset_search_buffer)
137
138 supports_suspend = Condition(lambda: hasattr(signal, "SIGTSTP"))
139 kb.add("c-z", filter=supports_suspend)(suspend_to_bg)
140
141 # Ctrl+I == Tab
142 kb.add(
143 "tab",
144 filter=(
145 has_focus(DEFAULT_BUFFER)
146 & ~has_selection
147 & insert_mode
148 & cursor_in_leading_ws
149 ),
150 )(indent_buffer)
151 kb.add("c-o", filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode))(
152 newline_autoindent_outer(shell.input_transformer_manager)
153 )
154
155 kb.add("f2", filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor)
156
157 @Condition
158 def auto_match():
159 return shell.auto_match
160
161 def all_quotes_paired(quote, buf):
162 paired = True
163 i = 0
164 while i < len(buf):
165 c = buf[i]
166 if c == quote:
167 paired = not paired
168 elif c == "\\":
169 i += 1
170 i += 1
171 return paired
172
173 focused_insert = (vi_insert_mode | emacs_insert_mode) & has_focus(DEFAULT_BUFFER)
174 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
175 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
176
177 def preceding_text(pattern: Union[str, Callable]):
178 if pattern in _preceding_text_cache:
179 return _preceding_text_cache[pattern]
180
181 if callable(pattern):
182
183 def _preceding_text():
184 app = get_app()
185 before_cursor = app.current_buffer.document.current_line_before_cursor
186 # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
187 return bool(pattern(before_cursor)) # type: ignore[operator]
188
53
54 def __post_init__(self):
55 if self.condition:
56 self.filter = filter_from_string(self.condition)
189 else:
57 else:
190 m = re.compile(pattern)
58 self.filter = None
191
192 def _preceding_text():
193 app = get_app()
194 before_cursor = app.current_buffer.document.current_line_before_cursor
195 return bool(m.match(before_cursor))
196
197 _preceding_text.__name__ = f"preceding_text({pattern!r})"
198
199 condition = Condition(_preceding_text)
200 _preceding_text_cache[pattern] = condition
201 return condition
202
203 def following_text(pattern):
204 try:
205 return _following_text_cache[pattern]
206 except KeyError:
207 pass
208 m = re.compile(pattern)
209
210 def _following_text():
211 app = get_app()
212 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
213
214 _following_text.__name__ = f"following_text({pattern!r})"
215
59
216 condition = Condition(_following_text)
217 _following_text_cache[pattern] = condition
218 return condition
219
60
220 @Condition
61 def create_identifier(handler: Callable):
221 def not_inside_unclosed_string():
62 parts = handler.__module__.split(".")
222 app = get_app()
63 name = handler.__name__
223 s = app.current_buffer.document.text_before_cursor
64 package = parts[0]
224 # remove escaped quotes
65 if len(parts) > 1:
225 s = s.replace('\\"', "").replace("\\'", "")
66 final_module = parts[-1]
226 # remove triple-quoted string literals
67 return f"{package}:{final_module}.{name}"
227 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
68 else:
228 # remove single-quoted string literals
69 return f"{package}:{name}"
229 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
230 return not ('"' in s or "'" in s)
231
232 # auto match
233 for key, cmd in match.auto_match_parens.items():
234 kb.add(key, filter=focused_insert & auto_match & following_text(r"[,)}\]]|$"))(
235 cmd
236 )
237
70
238 # raw string
239 for key, cmd in match.auto_match_parens_raw_string.items():
240 kb.add(
241 key,
242 filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$"),
243 )(cmd)
244
245 kb.add(
246 '"',
247 filter=focused_insert
248 & auto_match
249 & not_inside_unclosed_string
250 & preceding_text(lambda line: all_quotes_paired('"', line))
251 & following_text(r"[,)}\]]|$"),
252 )(match.double_quote)
253
254 kb.add(
255 "'",
256 filter=focused_insert
257 & auto_match
258 & not_inside_unclosed_string
259 & preceding_text(lambda line: all_quotes_paired("'", line))
260 & following_text(r"[,)}\]]|$"),
261 )(match.single_quote)
262
263 kb.add(
264 '"',
265 filter=focused_insert
266 & auto_match
267 & not_inside_unclosed_string
268 & preceding_text(r'^.*""$'),
269 )(match.docstring_double_quotes)
270
271 kb.add(
272 "'",
273 filter=focused_insert
274 & auto_match
275 & not_inside_unclosed_string
276 & preceding_text(r"^.*''$"),
277 )(match.docstring_single_quotes)
278
279 # just move cursor
280 kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)"))(
281 match.skip_over
282 )
283 kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]"))(
284 match.skip_over
285 )
286 kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}"))(
287 match.skip_over
288 )
289 kb.add('"', filter=focused_insert & auto_match & following_text('^"'))(
290 match.skip_over
291 )
292 kb.add("'", filter=focused_insert & auto_match & following_text("^'"))(
293 match.skip_over
294 )
295
71
296 kb.add(
72 AUTO_MATCH_BINDINGS = [
297 "backspace",
73 *[
298 filter=focused_insert
74 Binding(
299 & preceding_text(r".*\($")
75 cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
300 & auto_match
301 & following_text(r"^\)"),
302 )(match.delete_pair)
303 kb.add(
304 "backspace",
305 filter=focused_insert
306 & preceding_text(r".*\[$")
307 & auto_match
308 & following_text(r"^\]"),
309 )(match.delete_pair)
310 kb.add(
311 "backspace",
312 filter=focused_insert
313 & preceding_text(r".*\{$")
314 & auto_match
315 & following_text(r"^\}"),
316 )(match.delete_pair)
317 kb.add(
318 "backspace",
319 filter=focused_insert
320 & preceding_text('.*"$')
321 & auto_match
322 & following_text('^"'),
323 )(match.delete_pair)
324 kb.add(
325 "backspace",
326 filter=focused_insert
327 & preceding_text(r".*'$")
328 & auto_match
329 & following_text(r"^'"),
330 )(match.delete_pair)
331
332 if shell.display_completions == "readlinelike":
333 kb.add(
334 "c-i",
335 filter=(
336 has_focus(DEFAULT_BUFFER)
337 & ~has_selection
338 & insert_mode
339 & ~cursor_in_leading_ws
340 ),
341 )(display_completions_like_readline)
342
343 if sys.platform == "win32" or for_all_platforms:
344 kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste)
345
346 focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode
347
348 # autosuggestions
349 @Condition
350 def navigable_suggestions():
351 return isinstance(
352 shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory
353 )
76 )
354
77 for key, cmd in match.auto_match_parens.items()
355 kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))(
78 ],
356 auto_suggest.accept_in_vi_insert_mode
79 *[
357 )
80 # raw string
358 kb.add("c-e", filter=focused_insert_vi & ebivim)(
81 Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
359 auto_suggest.accept_in_vi_insert_mode
82 for key, cmd in match.auto_match_parens_raw_string.items()
360 )
83 ],
361 kb.add("c-f", filter=focused_insert_vi)(auto_suggest.accept)
84 Binding(
362 kb.add("escape", "f", filter=focused_insert_vi & ebivim)(auto_suggest.accept_word)
85 match.double_quote,
363 kb.add("c-right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
86 ['"'],
364 auto_suggest.accept_token
87 "focused_insert"
365 )
88 " & auto_match"
366 kb.add(
89 " & not_inside_unclosed_string"
367 "escape", filter=has_suggestion & has_focus(DEFAULT_BUFFER) & emacs_insert_mode
90 " & preceded_by_paired_double_quotes"
368 )(auto_suggest.discard)
91 " & followed_by_closing_paren_or_end",
369 kb.add(
92 ),
370 "up",
93 Binding(
371 filter=navigable_suggestions
94 match.single_quote,
372 & ~has_line_above
95 ["'"],
373 & has_suggestion
96 "focused_insert"
374 & has_focus(DEFAULT_BUFFER),
97 " & auto_match"
375 )(auto_suggest.swap_autosuggestion_up(shell.auto_suggest))
98 " & not_inside_unclosed_string"
376 kb.add(
99 " & preceded_by_paired_single_quotes"
377 "down",
100 " & followed_by_closing_paren_or_end",
378 filter=navigable_suggestions
101 ),
379 & ~has_line_below
102 Binding(
380 & has_suggestion
103 match.docstring_double_quotes,
381 & has_focus(DEFAULT_BUFFER),
104 ['"'],
382 )(auto_suggest.swap_autosuggestion_down(shell.auto_suggest))
105 "focused_insert"
383 kb.add(
106 " & auto_match"
384 "up", filter=has_line_above & navigable_suggestions & has_focus(DEFAULT_BUFFER)
107 " & not_inside_unclosed_string"
385 )(auto_suggest.up_and_update_hint)
108 " & preceded_by_two_double_quotes",
386 kb.add(
109 ),
387 "down",
110 Binding(
388 filter=has_line_below & navigable_suggestions & has_focus(DEFAULT_BUFFER),
111 match.docstring_single_quotes,
389 )(auto_suggest.down_and_update_hint)
112 ["'"],
390 kb.add("right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
113 "focused_insert"
391 auto_suggest.accept_character
114 " & auto_match"
392 )
115 " & not_inside_unclosed_string"
393 kb.add("c-left", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
116 " & preceded_by_two_single_quotes",
394 auto_suggest.accept_and_move_cursor_left
117 ),
395 )
118 Binding(
396 kb.add("c-down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
119 match.skip_over,
397 auto_suggest.accept_and_keep_cursor
120 [")"],
398 )
121 "focused_insert & auto_match & followed_by_closing_round_paren",
399 kb.add("backspace", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
122 ),
400 auto_suggest.backspace_and_resume_hint
123 Binding(
401 )
124 match.skip_over,
402
125 ["]"],
403 # Simple Control keybindings
126 "focused_insert & auto_match & followed_by_closing_bracket",
404 key_cmd_dict = {
127 ),
128 Binding(
129 match.skip_over,
130 ["}"],
131 "focused_insert & auto_match & followed_by_closing_brace",
132 ),
133 Binding(
134 match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
135 ),
136 Binding(
137 match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
138 ),
139 Binding(
140 match.delete_pair,
141 ["backspace"],
142 "focused_insert"
143 " & preceded_by_opening_round_paren"
144 " & auto_match"
145 " & followed_by_closing_round_paren",
146 ),
147 Binding(
148 match.delete_pair,
149 ["backspace"],
150 "focused_insert"
151 " & preceded_by_opening_bracket"
152 " & auto_match"
153 " & followed_by_closing_bracket",
154 ),
155 Binding(
156 match.delete_pair,
157 ["backspace"],
158 "focused_insert"
159 " & preceded_by_opening_brace"
160 " & auto_match"
161 " & followed_by_closing_brace",
162 ),
163 Binding(
164 match.delete_pair,
165 ["backspace"],
166 "focused_insert"
167 " & preceded_by_double_quote"
168 " & auto_match"
169 " & followed_by_double_quote",
170 ),
171 Binding(
172 match.delete_pair,
173 ["backspace"],
174 "focused_insert"
175 " & preceded_by_single_quote"
176 " & auto_match"
177 " & followed_by_single_quote",
178 ),
179 ]
180
181 AUTO_SUGGEST_BINDINGS = [
182 Binding(
183 auto_suggest.accept_in_vi_insert_mode,
184 ["end"],
185 "default_buffer_focused & (ebivim | ~vi_insert_mode)",
186 ),
187 Binding(
188 auto_suggest.accept_in_vi_insert_mode,
189 ["c-e"],
190 "vi_insert_mode & default_buffer_focused & ebivim",
191 ),
192 Binding(auto_suggest.accept, ["c-f"], "vi_insert_mode & default_buffer_focused"),
193 Binding(
194 auto_suggest.accept_word,
195 ["escape", "f"],
196 "vi_insert_mode & default_buffer_focused & ebivim",
197 ),
198 Binding(
199 auto_suggest.accept_token,
200 ["c-right"],
201 "has_suggestion & default_buffer_focused",
202 ),
203 Binding(
204 auto_suggest.discard,
205 ["escape"],
206 "has_suggestion & default_buffer_focused & emacs_insert_mode",
207 ),
208 Binding(
209 auto_suggest.swap_autosuggestion_up,
210 ["up"],
211 "navigable_suggestions"
212 " & ~has_line_above"
213 " & has_suggestion"
214 " & default_buffer_focused",
215 ),
216 Binding(
217 auto_suggest.swap_autosuggestion_down,
218 ["down"],
219 "navigable_suggestions"
220 " & ~has_line_below"
221 " & has_suggestion"
222 " & default_buffer_focused",
223 ),
224 Binding(
225 auto_suggest.up_and_update_hint,
226 ["up"],
227 "has_line_above & navigable_suggestions & default_buffer_focused",
228 ),
229 Binding(
230 auto_suggest.down_and_update_hint,
231 ["down"],
232 "has_line_below & navigable_suggestions & default_buffer_focused",
233 ),
234 Binding(
235 auto_suggest.accept_character,
236 ["escape", "right"],
237 "has_suggestion & default_buffer_focused",
238 ),
239 Binding(
240 auto_suggest.accept_and_move_cursor_left,
241 ["c-left"],
242 "has_suggestion & default_buffer_focused",
243 ),
244 Binding(
245 auto_suggest.accept_and_keep_cursor,
246 ["c-down"],
247 "has_suggestion & default_buffer_focused",
248 ),
249 Binding(
250 auto_suggest.backspace_and_resume_hint,
251 ["backspace"],
252 "has_suggestion & default_buffer_focused",
253 ),
254 ]
255
256
257 SIMPLE_CONTROL_BINDINGS = [
258 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
259 for key, cmd in {
405 "c-a": nc.beginning_of_line,
260 "c-a": nc.beginning_of_line,
406 "c-b": nc.backward_char,
261 "c-b": nc.backward_char,
407 "c-k": nc.kill_line,
262 "c-k": nc.kill_line,
408 "c-w": nc.backward_kill_word,
263 "c-w": nc.backward_kill_word,
409 "c-y": nc.yank,
264 "c-y": nc.yank,
410 "c-_": nc.undo,
265 "c-_": nc.undo,
411 }
266 }.items()
267 ]
412
268
413 for key, cmd in key_cmd_dict.items():
414 kb.add(key, filter=focused_insert_vi & ebivim)(cmd)
415
269
416 # Alt and Combo Control keybindings
270 ALT_AND_COMOBO_CONTROL_BINDINGS = [
417 keys_cmd_dict = {
271 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
272 for keys, cmd in {
418 # Control Combos
273 # Control Combos
419 ("c-x", "c-e"): nc.edit_and_execute,
274 ("c-x", "c-e"): nc.edit_and_execute,
420 ("c-x", "e"): nc.edit_and_execute,
275 ("c-x", "e"): nc.edit_and_execute,
421 # Alt
276 # Alt
422 ("escape", "b"): nc.backward_word,
277 ("escape", "b"): nc.backward_word,
423 ("escape", "c"): nc.capitalize_word,
278 ("escape", "c"): nc.capitalize_word,
424 ("escape", "d"): nc.kill_word,
279 ("escape", "d"): nc.kill_word,
425 ("escape", "h"): nc.backward_kill_word,
280 ("escape", "h"): nc.backward_kill_word,
426 ("escape", "l"): nc.downcase_word,
281 ("escape", "l"): nc.downcase_word,
427 ("escape", "u"): nc.uppercase_word,
282 ("escape", "u"): nc.uppercase_word,
428 ("escape", "y"): nc.yank_pop,
283 ("escape", "y"): nc.yank_pop,
429 ("escape", "."): nc.yank_last_arg,
284 ("escape", "."): nc.yank_last_arg,
430 }
285 }.items()
286 ]
287
288
289 def add_binding(bindings: KeyBindings, binding: Binding):
290 bindings.add(
291 *binding.keys,
292 **({"filter": binding.filter} if binding.filter is not None else {}),
293 )(binding.command)
294
295
296 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
297 """Set up the prompt_toolkit keyboard shortcuts for IPython.
431
298
432 for keys, cmd in keys_cmd_dict.items():
299 Parameters
433 kb.add(*keys, filter=focused_insert_vi & ebivim)(cmd)
300 ----------
301 shell: InteractiveShell
302 The current IPython shell Instance
303 skip: List[Binding]
304 Bindings to skip.
305
306 Returns
307 -------
308 KeyBindings
309 the keybinding instance for prompt toolkit.
310
311 """
312 kb = KeyBindings()
313 skip = skip or []
314 for binding in KEY_BINDINGS:
315 skip_this_one = False
316 for to_skip in skip:
317 if (
318 to_skip.command == binding.command
319 and to_skip.filter == binding.filter
320 and to_skip.keys == binding.keys
321 ):
322 skip_this_one = True
323 break
324 if skip_this_one:
325 continue
326 add_binding(kb, binding)
434
327
435 def get_input_mode(self):
328 def get_input_mode(self):
436 app = get_app()
329 app = get_app()
437 app.ttimeoutlen = shell.ttimeoutlen
330 app.ttimeoutlen = shell.ttimeoutlen
438 app.timeoutlen = shell.timeoutlen
331 app.timeoutlen = shell.timeoutlen
439
332
440 return self._input_mode
333 return self._input_mode
441
334
442 def set_input_mode(self, mode):
335 def set_input_mode(self, mode):
443 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
336 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
444 cursor = "\x1b[{} q".format(shape)
337 cursor = "\x1b[{} q".format(shape)
445
338
446 sys.stdout.write(cursor)
339 sys.stdout.write(cursor)
447 sys.stdout.flush()
340 sys.stdout.flush()
448
341
449 self._input_mode = mode
342 self._input_mode = mode
450
343
451 if shell.editing_mode == "vi" and shell.modal_cursor:
344 if shell.editing_mode == "vi" and shell.modal_cursor:
452 ViState._input_mode = InputMode.INSERT # type: ignore
345 ViState._input_mode = InputMode.INSERT # type: ignore
453 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
346 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
347
454 return kb
348 return kb
455
349
456
350
351 def reformat_and_execute(event):
352 """Reformat code and execute it"""
353 shell = get_ipython()
354 reformat_text_before_cursor(
355 event.current_buffer, event.current_buffer.document, shell
356 )
357 event.current_buffer.validate_and_handle()
358
359
457 def reformat_text_before_cursor(buffer, document, shell):
360 def reformat_text_before_cursor(buffer, document, shell):
458 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
361 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
459 try:
362 try:
460 formatted_text = shell.reformat_handler(text)
363 formatted_text = shell.reformat_handler(text)
461 buffer.insert_text(formatted_text)
364 buffer.insert_text(formatted_text)
462 except Exception as e:
365 except Exception as e:
463 buffer.insert_text(text)
366 buffer.insert_text(text)
464
367
465
368
369 def handle_return_or_newline_or_execute(event):
370 shell = get_ipython()
371 if getattr(shell, "handle_return", None):
372 return shell.handle_return(shell)(event)
373 else:
374 return newline_or_execute_outer(shell)(event)
375
376
466 def newline_or_execute_outer(shell):
377 def newline_or_execute_outer(shell):
467 def newline_or_execute(event):
378 def newline_or_execute(event):
468 """When the user presses return, insert a newline or execute the code."""
379 """When the user presses return, insert a newline or execute the code."""
469 b = event.current_buffer
380 b = event.current_buffer
470 d = b.document
381 d = b.document
471
382
472 if b.complete_state:
383 if b.complete_state:
473 cc = b.complete_state.current_completion
384 cc = b.complete_state.current_completion
474 if cc:
385 if cc:
475 b.apply_completion(cc)
386 b.apply_completion(cc)
476 else:
387 else:
477 b.cancel_completion()
388 b.cancel_completion()
478 return
389 return
479
390
480 # If there's only one line, treat it as if the cursor is at the end.
391 # If there's only one line, treat it as if the cursor is at the end.
481 # See https://github.com/ipython/ipython/issues/10425
392 # See https://github.com/ipython/ipython/issues/10425
482 if d.line_count == 1:
393 if d.line_count == 1:
483 check_text = d.text
394 check_text = d.text
484 else:
395 else:
485 check_text = d.text[: d.cursor_position]
396 check_text = d.text[: d.cursor_position]
486 status, indent = shell.check_complete(check_text)
397 status, indent = shell.check_complete(check_text)
487
398
488 # if all we have after the cursor is whitespace: reformat current text
399 # if all we have after the cursor is whitespace: reformat current text
489 # before cursor
400 # before cursor
490 after_cursor = d.text[d.cursor_position :]
401 after_cursor = d.text[d.cursor_position :]
491 reformatted = False
402 reformatted = False
492 if not after_cursor.strip():
403 if not after_cursor.strip():
493 reformat_text_before_cursor(b, d, shell)
404 reformat_text_before_cursor(b, d, shell)
494 reformatted = True
405 reformatted = True
495 if not (
406 if not (
496 d.on_last_line
407 d.on_last_line
497 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
408 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
498 ):
409 ):
499 if shell.autoindent:
410 if shell.autoindent:
500 b.insert_text("\n" + indent)
411 b.insert_text("\n" + indent)
501 else:
412 else:
502 b.insert_text("\n")
413 b.insert_text("\n")
503 return
414 return
504
415
505 if (status != "incomplete") and b.accept_handler:
416 if (status != "incomplete") and b.accept_handler:
506 if not reformatted:
417 if not reformatted:
507 reformat_text_before_cursor(b, d, shell)
418 reformat_text_before_cursor(b, d, shell)
508 b.validate_and_handle()
419 b.validate_and_handle()
509 else:
420 else:
510 if shell.autoindent:
421 if shell.autoindent:
511 b.insert_text("\n" + indent)
422 b.insert_text("\n" + indent)
512 else:
423 else:
513 b.insert_text("\n")
424 b.insert_text("\n")
514
425
515 newline_or_execute.__qualname__ = "newline_or_execute"
516
517 return newline_or_execute
426 return newline_or_execute
518
427
519
428
520 def previous_history_or_previous_completion(event):
429 def previous_history_or_previous_completion(event):
521 """
430 """
522 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
431 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
523
432
524 If completer is open this still select previous completion.
433 If completer is open this still select previous completion.
525 """
434 """
526 event.current_buffer.auto_up()
435 event.current_buffer.auto_up()
527
436
528
437
529 def next_history_or_next_completion(event):
438 def next_history_or_next_completion(event):
530 """
439 """
531 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
440 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
532
441
533 If completer is open this still select next completion.
442 If completer is open this still select next completion.
534 """
443 """
535 event.current_buffer.auto_down()
444 event.current_buffer.auto_down()
536
445
537
446
538 def dismiss_completion(event):
447 def dismiss_completion(event):
539 """Dismiss completion"""
448 """Dismiss completion"""
540 b = event.current_buffer
449 b = event.current_buffer
541 if b.complete_state:
450 if b.complete_state:
542 b.cancel_completion()
451 b.cancel_completion()
543
452
544
453
545 def reset_buffer(event):
454 def reset_buffer(event):
546 """Reset buffer"""
455 """Reset buffer"""
547 b = event.current_buffer
456 b = event.current_buffer
548 if b.complete_state:
457 if b.complete_state:
549 b.cancel_completion()
458 b.cancel_completion()
550 else:
459 else:
551 b.reset()
460 b.reset()
552
461
553
462
554 def reset_search_buffer(event):
463 def reset_search_buffer(event):
555 """Reset search buffer"""
464 """Reset search buffer"""
556 if event.current_buffer.document.text:
465 if event.current_buffer.document.text:
557 event.current_buffer.reset()
466 event.current_buffer.reset()
558 else:
467 else:
559 event.app.layout.focus(DEFAULT_BUFFER)
468 event.app.layout.focus(DEFAULT_BUFFER)
560
469
561
470
562 def suspend_to_bg(event):
471 def suspend_to_bg(event):
563 """Suspend to background"""
472 """Suspend to background"""
564 event.app.suspend_to_background()
473 event.app.suspend_to_background()
565
474
566
475
567 def quit(event):
476 def quit(event):
568 """
477 """
569 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
478 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
570
479
571 On platforms that support SIGQUIT, send SIGQUIT to the current process.
480 On platforms that support SIGQUIT, send SIGQUIT to the current process.
572 On other platforms, just exit the process with a message.
481 On other platforms, just exit the process with a message.
573 """
482 """
574 sigquit = getattr(signal, "SIGQUIT", None)
483 sigquit = getattr(signal, "SIGQUIT", None)
575 if sigquit is not None:
484 if sigquit is not None:
576 os.kill(0, signal.SIGQUIT)
485 os.kill(0, signal.SIGQUIT)
577 else:
486 else:
578 sys.exit("Quit")
487 sys.exit("Quit")
579
488
580
489
581 def indent_buffer(event):
490 def indent_buffer(event):
582 """Indent buffer"""
491 """Indent buffer"""
583 event.current_buffer.insert_text(" " * 4)
492 event.current_buffer.insert_text(" " * 4)
584
493
585
494
586 @undoc
495 @undoc
587 def newline_with_copy_margin(event):
496 def newline_with_copy_margin(event):
588 """
497 """
589 DEPRECATED since IPython 6.0
498 DEPRECATED since IPython 6.0
590
499
591 See :any:`newline_autoindent_outer` for a replacement.
500 See :any:`newline_autoindent_outer` for a replacement.
592
501
593 Preserve margin and cursor position when using
502 Preserve margin and cursor position when using
594 Control-O to insert a newline in EMACS mode
503 Control-O to insert a newline in EMACS mode
595 """
504 """
596 warnings.warn(
505 warnings.warn(
597 "`newline_with_copy_margin(event)` is deprecated since IPython 6.0. "
506 "`newline_with_copy_margin(event)` is deprecated since IPython 6.0. "
598 "see `newline_autoindent_outer(shell)(event)` for a replacement.",
507 "see `newline_autoindent_outer(shell)(event)` for a replacement.",
599 DeprecationWarning,
508 DeprecationWarning,
600 stacklevel=2,
509 stacklevel=2,
601 )
510 )
602
511
603 b = event.current_buffer
512 b = event.current_buffer
604 cursor_start_pos = b.document.cursor_position_col
513 cursor_start_pos = b.document.cursor_position_col
605 b.newline(copy_margin=True)
514 b.newline(copy_margin=True)
606 b.cursor_up(count=1)
515 b.cursor_up(count=1)
607 cursor_end_pos = b.document.cursor_position_col
516 cursor_end_pos = b.document.cursor_position_col
608 if cursor_start_pos != cursor_end_pos:
517 if cursor_start_pos != cursor_end_pos:
609 pos_diff = cursor_start_pos - cursor_end_pos
518 pos_diff = cursor_start_pos - cursor_end_pos
610 b.cursor_right(count=pos_diff)
519 b.cursor_right(count=pos_diff)
611
520
612
521
613 def newline_autoindent_outer(inputsplitter) -> Callable[..., None]:
522 def newline_autoindent(event):
614 """
523 """Insert a newline after the cursor indented appropriately.
615 Return a function suitable for inserting a indented newline after the cursor.
616
524
617 Fancier version of deprecated ``newline_with_copy_margin`` which should
525 Fancier version of deprecated ``newline_with_copy_margin`` which should
618 compute the correct indentation of the inserted line. That is to say, indent
526 compute the correct indentation of the inserted line. That is to say, indent
619 by 4 extra space after a function definition, class definition, context
527 by 4 extra space after a function definition, class definition, context
620 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
528 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
621 """
529 """
530 shell = get_ipython()
531 inputsplitter = shell.input_transformer_manager
532 b = event.current_buffer
533 d = b.document
622
534
623 def newline_autoindent(event):
535 if b.complete_state:
624 """Insert a newline after the cursor indented appropriately."""
536 b.cancel_completion()
625 b = event.current_buffer
537 text = d.text[: d.cursor_position] + "\n"
626 d = b.document
538 _, indent = inputsplitter.check_complete(text)
627
539 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
628 if b.complete_state:
629 b.cancel_completion()
630 text = d.text[: d.cursor_position] + "\n"
631 _, indent = inputsplitter.check_complete(text)
632 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
633
634 newline_autoindent.__qualname__ = "newline_autoindent"
635
636 return newline_autoindent
637
540
638
541
639 def open_input_in_editor(event):
542 def open_input_in_editor(event):
640 """Open code from input in external editor"""
543 """Open code from input in external editor"""
641 event.app.current_buffer.open_in_editor()
544 event.app.current_buffer.open_in_editor()
642
545
643
546
644 if sys.platform == "win32":
547 if sys.platform == "win32":
645 from IPython.core.error import TryNext
548 from IPython.core.error import TryNext
646 from IPython.lib.clipboard import (
549 from IPython.lib.clipboard import (
647 ClipboardEmpty,
550 ClipboardEmpty,
648 tkinter_clipboard_get,
551 tkinter_clipboard_get,
649 win32_clipboard_get,
552 win32_clipboard_get,
650 )
553 )
651
554
652 @undoc
555 @undoc
653 def win_paste(event):
556 def win_paste(event):
654 try:
557 try:
655 text = win32_clipboard_get()
558 text = win32_clipboard_get()
656 except TryNext:
559 except TryNext:
657 try:
560 try:
658 text = tkinter_clipboard_get()
561 text = tkinter_clipboard_get()
659 except (TryNext, ClipboardEmpty):
562 except (TryNext, ClipboardEmpty):
660 return
563 return
661 except ClipboardEmpty:
564 except ClipboardEmpty:
662 return
565 return
663 event.current_buffer.insert_text(text.replace("\t", " " * 4))
566 event.current_buffer.insert_text(text.replace("\t", " " * 4))
664
567
665 else:
568 else:
666
569
667 @undoc
570 @undoc
668 def win_paste(event):
571 def win_paste(event):
669 """Stub used when auto-generating shortcuts for documentation"""
572 """Stub used on other platforms"""
670 pass
573 pass
574
575
576 KEY_BINDINGS = [
577 Binding(
578 handle_return_or_newline_or_execute,
579 ["enter"],
580 "default_buffer_focused & ~has_selection & insert_mode",
581 ),
582 Binding(
583 reformat_and_execute,
584 ["escape", "enter"],
585 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
586 ),
587 Binding(quit, ["c-\\"]),
588 Binding(
589 previous_history_or_previous_completion,
590 ["c-p"],
591 "vi_insert_mode & default_buffer_focused",
592 ),
593 Binding(
594 next_history_or_next_completion,
595 ["c-n"],
596 "vi_insert_mode & default_buffer_focused",
597 ),
598 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
599 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
600 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
601 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
602 Binding(
603 indent_buffer,
604 ["tab"], # Ctrl+I == Tab
605 "default_buffer_focused"
606 " & ~has_selection"
607 " & insert_mode"
608 " & cursor_in_leading_ws",
609 ),
610 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
611 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
612 *AUTO_MATCH_BINDINGS,
613 *AUTO_SUGGEST_BINDINGS,
614 Binding(
615 display_completions_like_readline,
616 ["c-i"],
617 "readline_like_completions"
618 " & default_buffer_focused"
619 " & ~has_selection"
620 " & insert_mode"
621 " & ~cursor_in_leading_ws",
622 ),
623 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
624 *SIMPLE_CONTROL_BINDINGS,
625 *ALT_AND_COMOBO_CONTROL_BINDINGS,
626 ]
@@ -1,104 +1,104 b''
1 """
1 """
2 Utilities function for keybinding with prompt toolkit.
2 Utilities function for keybinding with prompt toolkit.
3
3
4 This will be bound to specific key press and filter modes,
4 This will be bound to specific key press and filter modes,
5 like whether we are in edit mode, and whether the completer is open.
5 like whether we are in edit mode, and whether the completer is open.
6 """
6 """
7 import re
7 import re
8 from prompt_toolkit.key_binding import KeyPressEvent
8 from prompt_toolkit.key_binding import KeyPressEvent
9
9
10
10
11 def parenthesis(event: KeyPressEvent):
11 def parenthesis(event: KeyPressEvent):
12 """Auto-close parenthesis"""
12 """Auto-close parenthesis"""
13 event.current_buffer.insert_text("()")
13 event.current_buffer.insert_text("()")
14 event.current_buffer.cursor_left()
14 event.current_buffer.cursor_left()
15
15
16
16
17 def brackets(event: KeyPressEvent):
17 def brackets(event: KeyPressEvent):
18 """Auto-close brackets"""
18 """Auto-close brackets"""
19 event.current_buffer.insert_text("[]")
19 event.current_buffer.insert_text("[]")
20 event.current_buffer.cursor_left()
20 event.current_buffer.cursor_left()
21
21
22
22
23 def braces(event: KeyPressEvent):
23 def braces(event: KeyPressEvent):
24 """Auto-close braces"""
24 """Auto-close braces"""
25 event.current_buffer.insert_text("{}")
25 event.current_buffer.insert_text("{}")
26 event.current_buffer.cursor_left()
26 event.current_buffer.cursor_left()
27
27
28
28
29 def double_quote(event: KeyPressEvent):
29 def double_quote(event: KeyPressEvent):
30 """Auto-close double quotes"""
30 """Auto-close double quotes"""
31 event.current_buffer.insert_text('""')
31 event.current_buffer.insert_text('""')
32 event.current_buffer.cursor_left()
32 event.current_buffer.cursor_left()
33
33
34
34
35 def single_quote(event: KeyPressEvent):
35 def single_quote(event: KeyPressEvent):
36 """Auto-close single quotes"""
36 """Auto-close single quotes"""
37 event.current_buffer.insert_text("''")
37 event.current_buffer.insert_text("''")
38 event.current_buffer.cursor_left()
38 event.current_buffer.cursor_left()
39
39
40
40
41 def docstring_double_quotes(event: KeyPressEvent):
41 def docstring_double_quotes(event: KeyPressEvent):
42 """Auto-close docstring (double quotes)"""
42 """Auto-close docstring (double quotes)"""
43 event.current_buffer.insert_text('""""')
43 event.current_buffer.insert_text('""""')
44 event.current_buffer.cursor_left(3)
44 event.current_buffer.cursor_left(3)
45
45
46
46
47 def docstring_single_quotes(event: KeyPressEvent):
47 def docstring_single_quotes(event: KeyPressEvent):
48 """Auto-close docstring (single quotes)"""
48 """Auto-close docstring (single quotes)"""
49 event.current_buffer.insert_text("''''")
49 event.current_buffer.insert_text("''''")
50 event.current_buffer.cursor_left(3)
50 event.current_buffer.cursor_left(3)
51
51
52
52
53 def raw_string_parenthesis(event: KeyPressEvent):
53 def raw_string_parenthesis(event: KeyPressEvent):
54 """Auto-close parenthesis in raw strings"""
54 """Auto-close parenthesis in raw strings"""
55 matches = re.match(
55 matches = re.match(
56 r".*(r|R)[\"'](-*)",
56 r".*(r|R)[\"'](-*)",
57 event.current_buffer.document.current_line_before_cursor,
57 event.current_buffer.document.current_line_before_cursor,
58 )
58 )
59 dashes = matches.group(2) if matches else ""
59 dashes = matches.group(2) if matches else ""
60 event.current_buffer.insert_text("()" + dashes)
60 event.current_buffer.insert_text("()" + dashes)
61 event.current_buffer.cursor_left(len(dashes) + 1)
61 event.current_buffer.cursor_left(len(dashes) + 1)
62
62
63
63
64 def raw_string_bracket(event: KeyPressEvent):
64 def raw_string_bracket(event: KeyPressEvent):
65 """Auto-close bracker in raw strings"""
65 """Auto-close bracker in raw strings"""
66 matches = re.match(
66 matches = re.match(
67 r".*(r|R)[\"'](-*)",
67 r".*(r|R)[\"'](-*)",
68 event.current_buffer.document.current_line_before_cursor,
68 event.current_buffer.document.current_line_before_cursor,
69 )
69 )
70 dashes = matches.group(2) if matches else ""
70 dashes = matches.group(2) if matches else ""
71 event.current_buffer.insert_text("[]" + dashes)
71 event.current_buffer.insert_text("[]" + dashes)
72 event.current_buffer.cursor_left(len(dashes) + 1)
72 event.current_buffer.cursor_left(len(dashes) + 1)
73
73
74
74
75 def raw_string_braces(event: KeyPressEvent):
75 def raw_string_braces(event: KeyPressEvent):
76 """Auto-close braces in raw strings"""
76 """Auto-close braces in raw strings"""
77 matches = re.match(
77 matches = re.match(
78 r".*(r|R)[\"'](-*)",
78 r".*(r|R)[\"'](-*)",
79 event.current_buffer.document.current_line_before_cursor,
79 event.current_buffer.document.current_line_before_cursor,
80 )
80 )
81 dashes = matches.group(2) if matches else ""
81 dashes = matches.group(2) if matches else ""
82 event.current_buffer.insert_text("{}" + dashes)
82 event.current_buffer.insert_text("{}" + dashes)
83 event.current_buffer.cursor_left(len(dashes) + 1)
83 event.current_buffer.cursor_left(len(dashes) + 1)
84
84
85
85
86 def skip_over(event: KeyPressEvent):
86 def skip_over(event: KeyPressEvent):
87 """Skip over automatically added parenthesis.
87 """Skip over automatically added parenthesis/quote.
88
88
89 (rather than adding another parenthesis)"""
89 (rather than adding another parenthesis/quote)"""
90 event.current_buffer.cursor_right()
90 event.current_buffer.cursor_right()
91
91
92
92
93 def delete_pair(event: KeyPressEvent):
93 def delete_pair(event: KeyPressEvent):
94 """Delete auto-closed parenthesis"""
94 """Delete auto-closed parenthesis"""
95 event.current_buffer.delete()
95 event.current_buffer.delete()
96 event.current_buffer.delete_before_cursor()
96 event.current_buffer.delete_before_cursor()
97
97
98
98
99 auto_match_parens = {"(": parenthesis, "[": brackets, "{": braces}
99 auto_match_parens = {"(": parenthesis, "[": brackets, "{": braces}
100 auto_match_parens_raw_string = {
100 auto_match_parens_raw_string = {
101 "(": raw_string_parenthesis,
101 "(": raw_string_parenthesis,
102 "[": raw_string_bracket,
102 "[": raw_string_bracket,
103 "{": raw_string_braces,
103 "{": raw_string_braces,
104 }
104 }
@@ -1,378 +1,375 b''
1 import re
1 import re
2 import tokenize
2 import tokenize
3 from io import StringIO
3 from io import StringIO
4 from typing import Callable, List, Optional, Union, Generator, Tuple, Sequence
4 from typing import Callable, List, Optional, Union, Generator, Tuple
5
5
6 from prompt_toolkit.buffer import Buffer
6 from prompt_toolkit.buffer import Buffer
7 from prompt_toolkit.key_binding import KeyPressEvent
7 from prompt_toolkit.key_binding import KeyPressEvent
8 from prompt_toolkit.key_binding.bindings import named_commands as nc
8 from prompt_toolkit.key_binding.bindings import named_commands as nc
9 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
9 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
10 from prompt_toolkit.document import Document
10 from prompt_toolkit.document import Document
11 from prompt_toolkit.history import History
11 from prompt_toolkit.history import History
12 from prompt_toolkit.shortcuts import PromptSession
12 from prompt_toolkit.shortcuts import PromptSession
13 from prompt_toolkit.layout.processors import (
13 from prompt_toolkit.layout.processors import (
14 Processor,
14 Processor,
15 Transformation,
15 Transformation,
16 TransformationInput,
16 TransformationInput,
17 )
17 )
18
18
19 from IPython.core.getipython import get_ipython
19 from IPython.utils.tokenutil import generate_tokens
20 from IPython.utils.tokenutil import generate_tokens
20
21
21
22
22 def _get_query(document: Document):
23 def _get_query(document: Document):
23 return document.lines[document.cursor_position_row]
24 return document.lines[document.cursor_position_row]
24
25
25
26
26 class AppendAutoSuggestionInAnyLine(Processor):
27 class AppendAutoSuggestionInAnyLine(Processor):
27 """
28 """
28 Append the auto suggestion to lines other than the last (appending to the
29 Append the auto suggestion to lines other than the last (appending to the
29 last line is natively supported by the prompt toolkit).
30 last line is natively supported by the prompt toolkit).
30 """
31 """
31
32
32 def __init__(self, style: str = "class:auto-suggestion") -> None:
33 def __init__(self, style: str = "class:auto-suggestion") -> None:
33 self.style = style
34 self.style = style
34
35
35 def apply_transformation(self, ti: TransformationInput) -> Transformation:
36 def apply_transformation(self, ti: TransformationInput) -> Transformation:
36 is_last_line = ti.lineno == ti.document.line_count - 1
37 is_last_line = ti.lineno == ti.document.line_count - 1
37 is_active_line = ti.lineno == ti.document.cursor_position_row
38 is_active_line = ti.lineno == ti.document.cursor_position_row
38
39
39 if not is_last_line and is_active_line:
40 if not is_last_line and is_active_line:
40 buffer = ti.buffer_control.buffer
41 buffer = ti.buffer_control.buffer
41
42
42 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
43 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
43 suggestion = buffer.suggestion.text
44 suggestion = buffer.suggestion.text
44 else:
45 else:
45 suggestion = ""
46 suggestion = ""
46
47
47 return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
48 return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
48 else:
49 else:
49 return Transformation(fragments=ti.fragments)
50 return Transformation(fragments=ti.fragments)
50
51
51
52
52 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
53 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
53 """
54 """
54 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
55 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
55 suggestion from history. To do so it remembers the current position, but it
56 suggestion from history. To do so it remembers the current position, but it
56 state need to carefully be cleared on the right events.
57 state need to carefully be cleared on the right events.
57 """
58 """
58
59
59 def __init__(
60 def __init__(
60 self,
61 self,
61 ):
62 ):
62 self.skip_lines = 0
63 self.skip_lines = 0
63 self._connected_apps = []
64 self._connected_apps = []
64
65
65 def reset_history_position(self, _: Buffer):
66 def reset_history_position(self, _: Buffer):
66 self.skip_lines = 0
67 self.skip_lines = 0
67
68
68 def disconnect(self):
69 def disconnect(self):
69 for pt_app in self._connected_apps:
70 for pt_app in self._connected_apps:
70 text_insert_event = pt_app.default_buffer.on_text_insert
71 text_insert_event = pt_app.default_buffer.on_text_insert
71 text_insert_event.remove_handler(self.reset_history_position)
72 text_insert_event.remove_handler(self.reset_history_position)
72
73
73 def connect(self, pt_app: PromptSession):
74 def connect(self, pt_app: PromptSession):
74 self._connected_apps.append(pt_app)
75 self._connected_apps.append(pt_app)
75 # note: `on_text_changed` could be used for a bit different behaviour
76 # note: `on_text_changed` could be used for a bit different behaviour
76 # on character deletion (i.e. reseting history position on backspace)
77 # on character deletion (i.e. reseting history position on backspace)
77 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
78 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
78 pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
79 pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
79
80
80 def get_suggestion(
81 def get_suggestion(
81 self, buffer: Buffer, document: Document
82 self, buffer: Buffer, document: Document
82 ) -> Optional[Suggestion]:
83 ) -> Optional[Suggestion]:
83 text = _get_query(document)
84 text = _get_query(document)
84
85
85 if text.strip():
86 if text.strip():
86 for suggestion, _ in self._find_next_match(
87 for suggestion, _ in self._find_next_match(
87 text, self.skip_lines, buffer.history
88 text, self.skip_lines, buffer.history
88 ):
89 ):
89 return Suggestion(suggestion)
90 return Suggestion(suggestion)
90
91
91 return None
92 return None
92
93
93 def _dismiss(self, buffer, *args, **kwargs):
94 def _dismiss(self, buffer, *args, **kwargs):
94 buffer.suggestion = None
95 buffer.suggestion = None
95
96
96 def _find_match(
97 def _find_match(
97 self, text: str, skip_lines: float, history: History, previous: bool
98 self, text: str, skip_lines: float, history: History, previous: bool
98 ) -> Generator[Tuple[str, float], None, None]:
99 ) -> Generator[Tuple[str, float], None, None]:
99 """
100 """
100 text : str
101 text : str
101 Text content to find a match for, the user cursor is most of the
102 Text content to find a match for, the user cursor is most of the
102 time at the end of this text.
103 time at the end of this text.
103 skip_lines : float
104 skip_lines : float
104 number of items to skip in the search, this is used to indicate how
105 number of items to skip in the search, this is used to indicate how
105 far in the list the user has navigated by pressing up or down.
106 far in the list the user has navigated by pressing up or down.
106 The float type is used as the base value is +inf
107 The float type is used as the base value is +inf
107 history : History
108 history : History
108 prompt_toolkit History instance to fetch previous entries from.
109 prompt_toolkit History instance to fetch previous entries from.
109 previous : bool
110 previous : bool
110 Direction of the search, whether we are looking previous match
111 Direction of the search, whether we are looking previous match
111 (True), or next match (False).
112 (True), or next match (False).
112
113
113 Yields
114 Yields
114 ------
115 ------
115 Tuple with:
116 Tuple with:
116 str:
117 str:
117 current suggestion.
118 current suggestion.
118 float:
119 float:
119 will actually yield only ints, which is passed back via skip_lines,
120 will actually yield only ints, which is passed back via skip_lines,
120 which may be a +inf (float)
121 which may be a +inf (float)
121
122
122
123
123 """
124 """
124 line_number = -1
125 line_number = -1
125 for string in reversed(list(history.get_strings())):
126 for string in reversed(list(history.get_strings())):
126 for line in reversed(string.splitlines()):
127 for line in reversed(string.splitlines()):
127 line_number += 1
128 line_number += 1
128 if not previous and line_number < skip_lines:
129 if not previous and line_number < skip_lines:
129 continue
130 continue
130 # do not return empty suggestions as these
131 # do not return empty suggestions as these
131 # close the auto-suggestion overlay (and are useless)
132 # close the auto-suggestion overlay (and are useless)
132 if line.startswith(text) and len(line) > len(text):
133 if line.startswith(text) and len(line) > len(text):
133 yield line[len(text) :], line_number
134 yield line[len(text) :], line_number
134 if previous and line_number >= skip_lines:
135 if previous and line_number >= skip_lines:
135 return
136 return
136
137
137 def _find_next_match(
138 def _find_next_match(
138 self, text: str, skip_lines: float, history: History
139 self, text: str, skip_lines: float, history: History
139 ) -> Generator[Tuple[str, float], None, None]:
140 ) -> Generator[Tuple[str, float], None, None]:
140 return self._find_match(text, skip_lines, history, previous=False)
141 return self._find_match(text, skip_lines, history, previous=False)
141
142
142 def _find_previous_match(self, text: str, skip_lines: float, history: History):
143 def _find_previous_match(self, text: str, skip_lines: float, history: History):
143 return reversed(
144 return reversed(
144 list(self._find_match(text, skip_lines, history, previous=True))
145 list(self._find_match(text, skip_lines, history, previous=True))
145 )
146 )
146
147
147 def up(self, query: str, other_than: str, history: History) -> None:
148 def up(self, query: str, other_than: str, history: History) -> None:
148 for suggestion, line_number in self._find_next_match(
149 for suggestion, line_number in self._find_next_match(
149 query, self.skip_lines, history
150 query, self.skip_lines, history
150 ):
151 ):
151 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
152 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
152 # we want to switch from 'very.b' to 'very.a' because a) if the
153 # we want to switch from 'very.b' to 'very.a' because a) if the
153 # suggestion equals current text, prompt-toolkit aborts suggesting
154 # suggestion equals current text, prompt-toolkit aborts suggesting
154 # b) user likely would not be interested in 'very' anyways (they
155 # b) user likely would not be interested in 'very' anyways (they
155 # already typed it).
156 # already typed it).
156 if query + suggestion != other_than:
157 if query + suggestion != other_than:
157 self.skip_lines = line_number
158 self.skip_lines = line_number
158 break
159 break
159 else:
160 else:
160 # no matches found, cycle back to beginning
161 # no matches found, cycle back to beginning
161 self.skip_lines = 0
162 self.skip_lines = 0
162
163
163 def down(self, query: str, other_than: str, history: History) -> None:
164 def down(self, query: str, other_than: str, history: History) -> None:
164 for suggestion, line_number in self._find_previous_match(
165 for suggestion, line_number in self._find_previous_match(
165 query, self.skip_lines, history
166 query, self.skip_lines, history
166 ):
167 ):
167 if query + suggestion != other_than:
168 if query + suggestion != other_than:
168 self.skip_lines = line_number
169 self.skip_lines = line_number
169 break
170 break
170 else:
171 else:
171 # no matches found, cycle to end
172 # no matches found, cycle to end
172 for suggestion, line_number in self._find_previous_match(
173 for suggestion, line_number in self._find_previous_match(
173 query, float("Inf"), history
174 query, float("Inf"), history
174 ):
175 ):
175 if query + suggestion != other_than:
176 if query + suggestion != other_than:
176 self.skip_lines = line_number
177 self.skip_lines = line_number
177 break
178 break
178
179
179
180
180 # Needed for to accept autosuggestions in vi insert mode
181 # Needed for to accept autosuggestions in vi insert mode
181 def accept_in_vi_insert_mode(event: KeyPressEvent):
182 def accept_in_vi_insert_mode(event: KeyPressEvent):
182 """Apply autosuggestion if at end of line."""
183 """Apply autosuggestion if at end of line."""
183 buffer = event.current_buffer
184 buffer = event.current_buffer
184 d = buffer.document
185 d = buffer.document
185 after_cursor = d.text[d.cursor_position :]
186 after_cursor = d.text[d.cursor_position :]
186 lines = after_cursor.split("\n")
187 lines = after_cursor.split("\n")
187 end_of_current_line = lines[0].strip()
188 end_of_current_line = lines[0].strip()
188 suggestion = buffer.suggestion
189 suggestion = buffer.suggestion
189 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 == ""):
190 buffer.insert_text(suggestion.text)
191 buffer.insert_text(suggestion.text)
191 else:
192 else:
192 nc.end_of_line(event)
193 nc.end_of_line(event)
193
194
194
195
195 def accept(event: KeyPressEvent):
196 def accept(event: KeyPressEvent):
196 """Accept autosuggestion"""
197 """Accept autosuggestion"""
197 buffer = event.current_buffer
198 buffer = event.current_buffer
198 suggestion = buffer.suggestion
199 suggestion = buffer.suggestion
199 if suggestion:
200 if suggestion:
200 buffer.insert_text(suggestion.text)
201 buffer.insert_text(suggestion.text)
201 else:
202 else:
202 nc.forward_char(event)
203 nc.forward_char(event)
203
204
204
205
205 def discard(event: KeyPressEvent):
206 def discard(event: KeyPressEvent):
206 """Discard autosuggestion"""
207 """Discard autosuggestion"""
207 buffer = event.current_buffer
208 buffer = event.current_buffer
208 buffer.suggestion = None
209 buffer.suggestion = None
209
210
210
211
211 def accept_word(event: KeyPressEvent):
212 def accept_word(event: KeyPressEvent):
212 """Fill partial autosuggestion by word"""
213 """Fill partial autosuggestion by word"""
213 buffer = event.current_buffer
214 buffer = event.current_buffer
214 suggestion = buffer.suggestion
215 suggestion = buffer.suggestion
215 if suggestion:
216 if suggestion:
216 t = re.split(r"(\S+\s+)", suggestion.text)
217 t = re.split(r"(\S+\s+)", suggestion.text)
217 buffer.insert_text(next((x for x in t if x), ""))
218 buffer.insert_text(next((x for x in t if x), ""))
218 else:
219 else:
219 nc.forward_word(event)
220 nc.forward_word(event)
220
221
221
222
222 def accept_character(event: KeyPressEvent):
223 def accept_character(event: KeyPressEvent):
223 """Fill partial autosuggestion by character"""
224 """Fill partial autosuggestion by character"""
224 b = event.current_buffer
225 b = event.current_buffer
225 suggestion = b.suggestion
226 suggestion = b.suggestion
226 if suggestion and suggestion.text:
227 if suggestion and suggestion.text:
227 b.insert_text(suggestion.text[0])
228 b.insert_text(suggestion.text[0])
228
229
229
230
230 def accept_and_keep_cursor(event: KeyPressEvent):
231 def accept_and_keep_cursor(event: KeyPressEvent):
231 """Accept autosuggestion and keep cursor in place"""
232 """Accept autosuggestion and keep cursor in place"""
232 buffer = event.current_buffer
233 buffer = event.current_buffer
233 old_position = buffer.cursor_position
234 old_position = buffer.cursor_position
234 suggestion = buffer.suggestion
235 suggestion = buffer.suggestion
235 if suggestion:
236 if suggestion:
236 buffer.insert_text(suggestion.text)
237 buffer.insert_text(suggestion.text)
237 buffer.cursor_position = old_position
238 buffer.cursor_position = old_position
238
239
239
240
240 def accept_and_move_cursor_left(event: KeyPressEvent):
241 def accept_and_move_cursor_left(event: KeyPressEvent):
241 """Accept autosuggestion and move cursor left in place"""
242 """Accept autosuggestion and move cursor left in place"""
242 accept_and_keep_cursor(event)
243 accept_and_keep_cursor(event)
243 nc.backward_char(event)
244 nc.backward_char(event)
244
245
245
246
246 def _update_hint(buffer: Buffer):
247 def _update_hint(buffer: Buffer):
247 if buffer.auto_suggest:
248 if buffer.auto_suggest:
248 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
249 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
249 buffer.suggestion = suggestion
250 buffer.suggestion = suggestion
250
251
251
252
252 def backspace_and_resume_hint(event: KeyPressEvent):
253 def backspace_and_resume_hint(event: KeyPressEvent):
253 """Resume autosuggestions after deleting last character"""
254 """Resume autosuggestions after deleting last character"""
254 current_buffer = event.current_buffer
255 current_buffer = event.current_buffer
255
256
256 def resume_hinting(buffer: Buffer):
257 def resume_hinting(buffer: Buffer):
257 _update_hint(buffer)
258 _update_hint(buffer)
258 current_buffer.on_text_changed.remove_handler(resume_hinting)
259 current_buffer.on_text_changed.remove_handler(resume_hinting)
259
260
260 current_buffer.on_text_changed.add_handler(resume_hinting)
261 current_buffer.on_text_changed.add_handler(resume_hinting)
261 nc.backward_delete_char(event)
262 nc.backward_delete_char(event)
262
263
263
264
264 def up_and_update_hint(event: KeyPressEvent):
265 def up_and_update_hint(event: KeyPressEvent):
265 """Go up and update hint"""
266 """Go up and update hint"""
266 current_buffer = event.current_buffer
267 current_buffer = event.current_buffer
267
268
268 current_buffer.auto_up(count=event.arg)
269 current_buffer.auto_up(count=event.arg)
269 _update_hint(current_buffer)
270 _update_hint(current_buffer)
270
271
271
272
272 def down_and_update_hint(event: KeyPressEvent):
273 def down_and_update_hint(event: KeyPressEvent):
273 """Go down and update hint"""
274 """Go down and update hint"""
274 current_buffer = event.current_buffer
275 current_buffer = event.current_buffer
275
276
276 current_buffer.auto_down(count=event.arg)
277 current_buffer.auto_down(count=event.arg)
277 _update_hint(current_buffer)
278 _update_hint(current_buffer)
278
279
279
280
280 def accept_token(event: KeyPressEvent):
281 def accept_token(event: KeyPressEvent):
281 """Fill partial autosuggestion by token"""
282 """Fill partial autosuggestion by token"""
282 b = event.current_buffer
283 b = event.current_buffer
283 suggestion = b.suggestion
284 suggestion = b.suggestion
284
285
285 if suggestion:
286 if suggestion:
286 prefix = _get_query(b.document)
287 prefix = _get_query(b.document)
287 text = prefix + suggestion.text
288 text = prefix + suggestion.text
288
289
289 tokens: List[Optional[str]] = [None, None, None]
290 tokens: List[Optional[str]] = [None, None, None]
290 substrings = [""]
291 substrings = [""]
291 i = 0
292 i = 0
292
293
293 for token in generate_tokens(StringIO(text).readline):
294 for token in generate_tokens(StringIO(text).readline):
294 if token.type == tokenize.NEWLINE:
295 if token.type == tokenize.NEWLINE:
295 index = len(text)
296 index = len(text)
296 else:
297 else:
297 index = text.index(token[1], len(substrings[-1]))
298 index = text.index(token[1], len(substrings[-1]))
298 substrings.append(text[:index])
299 substrings.append(text[:index])
299 tokenized_so_far = substrings[-1]
300 tokenized_so_far = substrings[-1]
300 if tokenized_so_far.startswith(prefix):
301 if tokenized_so_far.startswith(prefix):
301 if i == 0 and len(tokenized_so_far) > len(prefix):
302 if i == 0 and len(tokenized_so_far) > len(prefix):
302 tokens[0] = tokenized_so_far[len(prefix) :]
303 tokens[0] = tokenized_so_far[len(prefix) :]
303 substrings.append(tokenized_so_far)
304 substrings.append(tokenized_so_far)
304 i += 1
305 i += 1
305 tokens[i] = token[1]
306 tokens[i] = token[1]
306 if i == 2:
307 if i == 2:
307 break
308 break
308 i += 1
309 i += 1
309
310
310 if tokens[0]:
311 if tokens[0]:
311 to_insert: str
312 to_insert: str
312 insert_text = substrings[-2]
313 insert_text = substrings[-2]
313 if tokens[1] and len(tokens[1]) == 1:
314 if tokens[1] and len(tokens[1]) == 1:
314 insert_text = substrings[-1]
315 insert_text = substrings[-1]
315 to_insert = insert_text[len(prefix) :]
316 to_insert = insert_text[len(prefix) :]
316 b.insert_text(to_insert)
317 b.insert_text(to_insert)
317 return
318 return
318
319
319 nc.forward_word(event)
320 nc.forward_word(event)
320
321
321
322
322 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
323 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
323
324
324
325
325 def _swap_autosuggestion(
326 def _swap_autosuggestion(
326 buffer: Buffer,
327 buffer: Buffer,
327 provider: NavigableAutoSuggestFromHistory,
328 provider: NavigableAutoSuggestFromHistory,
328 direction_method: Callable,
329 direction_method: Callable,
329 ):
330 ):
330 """
331 """
331 We skip most recent history entry (in either direction) if it equals the
332 We skip most recent history entry (in either direction) if it equals the
332 current autosuggestion because if user cycles when auto-suggestion is shown
333 current autosuggestion because if user cycles when auto-suggestion is shown
333 they most likely want something else than what was suggested (otherwise
334 they most likely want something else than what was suggested (otherwise
334 they would have accepted the suggestion).
335 they would have accepted the suggestion).
335 """
336 """
336 suggestion = buffer.suggestion
337 suggestion = buffer.suggestion
337 if not suggestion:
338 if not suggestion:
338 return
339 return
339
340
340 query = _get_query(buffer.document)
341 query = _get_query(buffer.document)
341 current = query + suggestion.text
342 current = query + suggestion.text
342
343
343 direction_method(query=query, other_than=current, history=buffer.history)
344 direction_method(query=query, other_than=current, history=buffer.history)
344
345
345 new_suggestion = provider.get_suggestion(buffer, buffer.document)
346 new_suggestion = provider.get_suggestion(buffer, buffer.document)
346 buffer.suggestion = new_suggestion
347 buffer.suggestion = new_suggestion
347
348
348
349
349 def swap_autosuggestion_up(provider: Provider):
350 def swap_autosuggestion_up(event: KeyPressEvent):
350 def swap_autosuggestion_up(event: KeyPressEvent):
351 """Get next autosuggestion from history."""
351 """Get next autosuggestion from history."""
352 shell = get_ipython()
352 if not isinstance(provider, NavigableAutoSuggestFromHistory):
353 provider = shell.auto_suggest
353 return
354
354
355 return _swap_autosuggestion(
355 if not isinstance(provider, NavigableAutoSuggestFromHistory):
356 buffer=event.current_buffer, provider=provider, direction_method=provider.up
356 return
357 )
358
357
359 swap_autosuggestion_up.__name__ = "swap_autosuggestion_up"
358 return _swap_autosuggestion(
360 return swap_autosuggestion_up
359 buffer=event.current_buffer, provider=provider, direction_method=provider.up
360 )
361
361
362
362
363 def swap_autosuggestion_down(
363 def swap_autosuggestion_down(event: KeyPressEvent):
364 provider: Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
364 """Get previous autosuggestion from history."""
365 ):
365 shell = get_ipython()
366 def swap_autosuggestion_down(event: KeyPressEvent):
366 provider = shell.auto_suggest
367 """Get previous autosuggestion from history."""
368 if not isinstance(provider, NavigableAutoSuggestFromHistory):
369 return
370
367
371 return _swap_autosuggestion(
368 if not isinstance(provider, NavigableAutoSuggestFromHistory):
372 buffer=event.current_buffer,
369 return
373 provider=provider,
374 direction_method=provider.down,
375 )
376
370
377 swap_autosuggestion_down.__name__ = "swap_autosuggestion_down"
371 return _swap_autosuggestion(
378 return swap_autosuggestion_down
372 buffer=event.current_buffer,
373 provider=provider,
374 direction_method=provider.down,
375 )
@@ -1,256 +1,255 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """Tests for the TerminalInteractiveShell and related pieces."""
2 """Tests for the TerminalInteractiveShell and related pieces."""
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 import sys
6 import sys
7 import unittest
7 import unittest
8 import os
8 import os
9
9
10 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
10 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
11
11
12 from IPython.core.inputtransformer import InputTransformer
12
13 from IPython.testing import tools as tt
13 from IPython.testing import tools as tt
14 from IPython.utils.capture import capture_output
15
14
16 from IPython.terminal.ptutils import _elide, _adjust_completion_text_based_on_context
15 from IPython.terminal.ptutils import _elide, _adjust_completion_text_based_on_context
17 from IPython.terminal.shortcuts.auto_suggest import NavigableAutoSuggestFromHistory
16 from IPython.terminal.shortcuts.auto_suggest import NavigableAutoSuggestFromHistory
18
17
19
18
20 class TestAutoSuggest(unittest.TestCase):
19 class TestAutoSuggest(unittest.TestCase):
21 def test_changing_provider(self):
20 def test_changing_provider(self):
22 ip = get_ipython()
21 ip = get_ipython()
23 ip.autosuggestions_provider = None
22 ip.autosuggestions_provider = None
24 self.assertEqual(ip.auto_suggest, None)
23 self.assertEqual(ip.auto_suggest, None)
25 ip.autosuggestions_provider = "AutoSuggestFromHistory"
24 ip.autosuggestions_provider = "AutoSuggestFromHistory"
26 self.assertIsInstance(ip.auto_suggest, AutoSuggestFromHistory)
25 self.assertIsInstance(ip.auto_suggest, AutoSuggestFromHistory)
27 ip.autosuggestions_provider = "NavigableAutoSuggestFromHistory"
26 ip.autosuggestions_provider = "NavigableAutoSuggestFromHistory"
28 self.assertIsInstance(ip.auto_suggest, NavigableAutoSuggestFromHistory)
27 self.assertIsInstance(ip.auto_suggest, NavigableAutoSuggestFromHistory)
29
28
30
29
31 class TestElide(unittest.TestCase):
30 class TestElide(unittest.TestCase):
32 def test_elide(self):
31 def test_elide(self):
33 _elide("concatenate((a1, a2, ...), axis", "") # do not raise
32 _elide("concatenate((a1, a2, ...), axis", "") # do not raise
34 _elide("concatenate((a1, a2, ..), . axis", "") # do not raise
33 _elide("concatenate((a1, a2, ..), . axis", "") # do not raise
35 self.assertEqual(
34 self.assertEqual(
36 _elide("aaaa.bbbb.ccccc.dddddd.eeeee.fffff.gggggg.hhhhhh", ""),
35 _elide("aaaa.bbbb.ccccc.dddddd.eeeee.fffff.gggggg.hhhhhh", ""),
37 "aaaa.b…g.hhhhhh",
36 "aaaa.b…g.hhhhhh",
38 )
37 )
39
38
40 test_string = os.sep.join(["", 10 * "a", 10 * "b", 10 * "c", ""])
39 test_string = os.sep.join(["", 10 * "a", 10 * "b", 10 * "c", ""])
41 expect_string = (
40 expect_string = (
42 os.sep + "a" + "\N{HORIZONTAL ELLIPSIS}" + "b" + os.sep + 10 * "c"
41 os.sep + "a" + "\N{HORIZONTAL ELLIPSIS}" + "b" + os.sep + 10 * "c"
43 )
42 )
44 self.assertEqual(_elide(test_string, ""), expect_string)
43 self.assertEqual(_elide(test_string, ""), expect_string)
45
44
46 def test_elide_typed_normal(self):
45 def test_elide_typed_normal(self):
47 self.assertEqual(
46 self.assertEqual(
48 _elide(
47 _elide(
49 "the quick brown fox jumped over the lazy dog",
48 "the quick brown fox jumped over the lazy dog",
50 "the quick brown fox",
49 "the quick brown fox",
51 min_elide=10,
50 min_elide=10,
52 ),
51 ),
53 "the…fox jumped over the lazy dog",
52 "the…fox jumped over the lazy dog",
54 )
53 )
55
54
56 def test_elide_typed_short_match(self):
55 def test_elide_typed_short_match(self):
57 """
56 """
58 if the match is too short we don't elide.
57 if the match is too short we don't elide.
59 avoid the "the...the"
58 avoid the "the...the"
60 """
59 """
61 self.assertEqual(
60 self.assertEqual(
62 _elide("the quick brown fox jumped over the lazy dog", "the", min_elide=10),
61 _elide("the quick brown fox jumped over the lazy dog", "the", min_elide=10),
63 "the quick brown fox jumped over the lazy dog",
62 "the quick brown fox jumped over the lazy dog",
64 )
63 )
65
64
66 def test_elide_typed_no_match(self):
65 def test_elide_typed_no_match(self):
67 """
66 """
68 if the match is too short we don't elide.
67 if the match is too short we don't elide.
69 avoid the "the...the"
68 avoid the "the...the"
70 """
69 """
71 # here we typed red instead of brown
70 # here we typed red instead of brown
72 self.assertEqual(
71 self.assertEqual(
73 _elide(
72 _elide(
74 "the quick brown fox jumped over the lazy dog",
73 "the quick brown fox jumped over the lazy dog",
75 "the quick red fox",
74 "the quick red fox",
76 min_elide=10,
75 min_elide=10,
77 ),
76 ),
78 "the quick brown fox jumped over the lazy dog",
77 "the quick brown fox jumped over the lazy dog",
79 )
78 )
80
79
81
80
82 class TestContextAwareCompletion(unittest.TestCase):
81 class TestContextAwareCompletion(unittest.TestCase):
83 def test_adjust_completion_text_based_on_context(self):
82 def test_adjust_completion_text_based_on_context(self):
84 # Adjusted case
83 # Adjusted case
85 self.assertEqual(
84 self.assertEqual(
86 _adjust_completion_text_based_on_context("arg1=", "func1(a=)", 7), "arg1"
85 _adjust_completion_text_based_on_context("arg1=", "func1(a=)", 7), "arg1"
87 )
86 )
88
87
89 # Untouched cases
88 # Untouched cases
90 self.assertEqual(
89 self.assertEqual(
91 _adjust_completion_text_based_on_context("arg1=", "func1(a)", 7), "arg1="
90 _adjust_completion_text_based_on_context("arg1=", "func1(a)", 7), "arg1="
92 )
91 )
93 self.assertEqual(
92 self.assertEqual(
94 _adjust_completion_text_based_on_context("arg1=", "func1(a", 7), "arg1="
93 _adjust_completion_text_based_on_context("arg1=", "func1(a", 7), "arg1="
95 )
94 )
96 self.assertEqual(
95 self.assertEqual(
97 _adjust_completion_text_based_on_context("%magic", "func1(a=)", 7), "%magic"
96 _adjust_completion_text_based_on_context("%magic", "func1(a=)", 7), "%magic"
98 )
97 )
99 self.assertEqual(
98 self.assertEqual(
100 _adjust_completion_text_based_on_context("func2", "func1(a=)", 7), "func2"
99 _adjust_completion_text_based_on_context("func2", "func1(a=)", 7), "func2"
101 )
100 )
102
101
103
102
104 # Decorator for interaction loop tests -----------------------------------------
103 # Decorator for interaction loop tests -----------------------------------------
105
104
106
105
107 class mock_input_helper(object):
106 class mock_input_helper(object):
108 """Machinery for tests of the main interact loop.
107 """Machinery for tests of the main interact loop.
109
108
110 Used by the mock_input decorator.
109 Used by the mock_input decorator.
111 """
110 """
112 def __init__(self, testgen):
111 def __init__(self, testgen):
113 self.testgen = testgen
112 self.testgen = testgen
114 self.exception = None
113 self.exception = None
115 self.ip = get_ipython()
114 self.ip = get_ipython()
116
115
117 def __enter__(self):
116 def __enter__(self):
118 self.orig_prompt_for_code = self.ip.prompt_for_code
117 self.orig_prompt_for_code = self.ip.prompt_for_code
119 self.ip.prompt_for_code = self.fake_input
118 self.ip.prompt_for_code = self.fake_input
120 return self
119 return self
121
120
122 def __exit__(self, etype, value, tb):
121 def __exit__(self, etype, value, tb):
123 self.ip.prompt_for_code = self.orig_prompt_for_code
122 self.ip.prompt_for_code = self.orig_prompt_for_code
124
123
125 def fake_input(self):
124 def fake_input(self):
126 try:
125 try:
127 return next(self.testgen)
126 return next(self.testgen)
128 except StopIteration:
127 except StopIteration:
129 self.ip.keep_running = False
128 self.ip.keep_running = False
130 return u''
129 return u''
131 except:
130 except:
132 self.exception = sys.exc_info()
131 self.exception = sys.exc_info()
133 self.ip.keep_running = False
132 self.ip.keep_running = False
134 return u''
133 return u''
135
134
136 def mock_input(testfunc):
135 def mock_input(testfunc):
137 """Decorator for tests of the main interact loop.
136 """Decorator for tests of the main interact loop.
138
137
139 Write the test as a generator, yield-ing the input strings, which IPython
138 Write the test as a generator, yield-ing the input strings, which IPython
140 will see as if they were typed in at the prompt.
139 will see as if they were typed in at the prompt.
141 """
140 """
142 def test_method(self):
141 def test_method(self):
143 testgen = testfunc(self)
142 testgen = testfunc(self)
144 with mock_input_helper(testgen) as mih:
143 with mock_input_helper(testgen) as mih:
145 mih.ip.interact()
144 mih.ip.interact()
146
145
147 if mih.exception is not None:
146 if mih.exception is not None:
148 # Re-raise captured exception
147 # Re-raise captured exception
149 etype, value, tb = mih.exception
148 etype, value, tb = mih.exception
150 import traceback
149 import traceback
151 traceback.print_tb(tb, file=sys.stdout)
150 traceback.print_tb(tb, file=sys.stdout)
152 del tb # Avoid reference loop
151 del tb # Avoid reference loop
153 raise value
152 raise value
154
153
155 return test_method
154 return test_method
156
155
157 # Test classes -----------------------------------------------------------------
156 # Test classes -----------------------------------------------------------------
158
157
159 class InteractiveShellTestCase(unittest.TestCase):
158 class InteractiveShellTestCase(unittest.TestCase):
160 def rl_hist_entries(self, rl, n):
159 def rl_hist_entries(self, rl, n):
161 """Get last n readline history entries as a list"""
160 """Get last n readline history entries as a list"""
162 return [rl.get_history_item(rl.get_current_history_length() - x)
161 return [rl.get_history_item(rl.get_current_history_length() - x)
163 for x in range(n - 1, -1, -1)]
162 for x in range(n - 1, -1, -1)]
164
163
165 @mock_input
164 @mock_input
166 def test_inputtransformer_syntaxerror(self):
165 def test_inputtransformer_syntaxerror(self):
167 ip = get_ipython()
166 ip = get_ipython()
168 ip.input_transformers_post.append(syntax_error_transformer)
167 ip.input_transformers_post.append(syntax_error_transformer)
169
168
170 try:
169 try:
171 #raise Exception
170 #raise Exception
172 with tt.AssertPrints('4', suppress=False):
171 with tt.AssertPrints('4', suppress=False):
173 yield u'print(2*2)'
172 yield u'print(2*2)'
174
173
175 with tt.AssertPrints('SyntaxError: input contains', suppress=False):
174 with tt.AssertPrints('SyntaxError: input contains', suppress=False):
176 yield u'print(2345) # syntaxerror'
175 yield u'print(2345) # syntaxerror'
177
176
178 with tt.AssertPrints('16', suppress=False):
177 with tt.AssertPrints('16', suppress=False):
179 yield u'print(4*4)'
178 yield u'print(4*4)'
180
179
181 finally:
180 finally:
182 ip.input_transformers_post.remove(syntax_error_transformer)
181 ip.input_transformers_post.remove(syntax_error_transformer)
183
182
184 def test_repl_not_plain_text(self):
183 def test_repl_not_plain_text(self):
185 ip = get_ipython()
184 ip = get_ipython()
186 formatter = ip.display_formatter
185 formatter = ip.display_formatter
187 assert formatter.active_types == ['text/plain']
186 assert formatter.active_types == ['text/plain']
188
187
189 # terminal may have arbitrary mimetype handler to open external viewer
188 # terminal may have arbitrary mimetype handler to open external viewer
190 # or inline images.
189 # or inline images.
191 assert formatter.ipython_display_formatter.enabled
190 assert formatter.ipython_display_formatter.enabled
192
191
193 class Test(object):
192 class Test(object):
194 def __repr__(self):
193 def __repr__(self):
195 return "<Test %i>" % id(self)
194 return "<Test %i>" % id(self)
196
195
197 def _repr_html_(self):
196 def _repr_html_(self):
198 return '<html>'
197 return '<html>'
199
198
200 # verify that HTML repr isn't computed
199 # verify that HTML repr isn't computed
201 obj = Test()
200 obj = Test()
202 data, _ = formatter.format(obj)
201 data, _ = formatter.format(obj)
203 self.assertEqual(data, {'text/plain': repr(obj)})
202 self.assertEqual(data, {'text/plain': repr(obj)})
204
203
205 class Test2(Test):
204 class Test2(Test):
206 def _ipython_display_(self):
205 def _ipython_display_(self):
207 from IPython.display import display, HTML
206 from IPython.display import display, HTML
208
207
209 display(HTML("<custom>"))
208 display(HTML("<custom>"))
210
209
211 # verify that mimehandlers are called
210 # verify that mimehandlers are called
212 called = False
211 called = False
213
212
214 def handler(data, metadata):
213 def handler(data, metadata):
215 print("Handler called")
214 print("Handler called")
216 nonlocal called
215 nonlocal called
217 called = True
216 called = True
218
217
219 ip.display_formatter.active_types.append("text/html")
218 ip.display_formatter.active_types.append("text/html")
220 ip.display_formatter.formatters["text/html"].enabled = True
219 ip.display_formatter.formatters["text/html"].enabled = True
221 ip.mime_renderers["text/html"] = handler
220 ip.mime_renderers["text/html"] = handler
222 try:
221 try:
223 obj = Test()
222 obj = Test()
224 display(obj)
223 display(obj)
225 finally:
224 finally:
226 ip.display_formatter.formatters["text/html"].enabled = False
225 ip.display_formatter.formatters["text/html"].enabled = False
227 del ip.mime_renderers["text/html"]
226 del ip.mime_renderers["text/html"]
228
227
229 assert called == True
228 assert called == True
230
229
231
230
232 def syntax_error_transformer(lines):
231 def syntax_error_transformer(lines):
233 """Transformer that throws SyntaxError if 'syntaxerror' is in the code."""
232 """Transformer that throws SyntaxError if 'syntaxerror' is in the code."""
234 for line in lines:
233 for line in lines:
235 pos = line.find('syntaxerror')
234 pos = line.find('syntaxerror')
236 if pos >= 0:
235 if pos >= 0:
237 e = SyntaxError('input contains "syntaxerror"')
236 e = SyntaxError('input contains "syntaxerror"')
238 e.text = line
237 e.text = line
239 e.offset = pos + 1
238 e.offset = pos + 1
240 raise e
239 raise e
241 return lines
240 return lines
242
241
243
242
244 class TerminalMagicsTestCase(unittest.TestCase):
243 class TerminalMagicsTestCase(unittest.TestCase):
245 def test_paste_magics_blankline(self):
244 def test_paste_magics_blankline(self):
246 """Test that code with a blank line doesn't get split (gh-3246)."""
245 """Test that code with a blank line doesn't get split (gh-3246)."""
247 ip = get_ipython()
246 ip = get_ipython()
248 s = ('def pasted_func(a):\n'
247 s = ('def pasted_func(a):\n'
249 ' b = a+1\n'
248 ' b = a+1\n'
250 '\n'
249 '\n'
251 ' return b')
250 ' return b')
252
251
253 tm = ip.magics_manager.registry['TerminalMagics']
252 tm = ip.magics_manager.registry['TerminalMagics']
254 tm.store_or_execute(s, name=None)
253 tm.store_or_execute(s, name=None)
255
254
256 self.assertEqual(ip.user_ns['pasted_func'](54), 55)
255 self.assertEqual(ip.user_ns['pasted_func'](54), 55)
@@ -1,318 +1,461 b''
1 import pytest
1 import pytest
2 from IPython.terminal.shortcuts.auto_suggest import (
2 from IPython.terminal.shortcuts.auto_suggest import (
3 accept,
3 accept,
4 accept_in_vi_insert_mode,
4 accept_in_vi_insert_mode,
5 accept_token,
5 accept_token,
6 accept_character,
6 accept_character,
7 accept_word,
7 accept_word,
8 accept_and_keep_cursor,
8 accept_and_keep_cursor,
9 discard,
9 discard,
10 NavigableAutoSuggestFromHistory,
10 NavigableAutoSuggestFromHistory,
11 swap_autosuggestion_up,
11 swap_autosuggestion_up,
12 swap_autosuggestion_down,
12 swap_autosuggestion_down,
13 )
13 )
14 from IPython.terminal.shortcuts.auto_match import skip_over
15 from IPython.terminal.shortcuts import create_ipython_shortcuts
14
16
15 from prompt_toolkit.history import InMemoryHistory
17 from prompt_toolkit.history import InMemoryHistory
16 from prompt_toolkit.buffer import Buffer
18 from prompt_toolkit.buffer import Buffer
17 from prompt_toolkit.document import Document
19 from prompt_toolkit.document import Document
18 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
20 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
19
21
20 from unittest.mock import patch, Mock
22 from unittest.mock import patch, Mock
21
23
22
24
23 def make_event(text, cursor, suggestion):
25 def make_event(text, cursor, suggestion):
24 event = Mock()
26 event = Mock()
25 event.current_buffer = Mock()
27 event.current_buffer = Mock()
26 event.current_buffer.suggestion = Mock()
28 event.current_buffer.suggestion = Mock()
27 event.current_buffer.text = text
29 event.current_buffer.text = text
28 event.current_buffer.cursor_position = cursor
30 event.current_buffer.cursor_position = cursor
29 event.current_buffer.suggestion.text = suggestion
31 event.current_buffer.suggestion.text = suggestion
30 event.current_buffer.document = Document(text=text, cursor_position=cursor)
32 event.current_buffer.document = Document(text=text, cursor_position=cursor)
31 return event
33 return event
32
34
33
35
34 @pytest.mark.parametrize(
36 @pytest.mark.parametrize(
35 "text, suggestion, expected",
37 "text, suggestion, expected",
36 [
38 [
37 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):"),
39 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):"),
38 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):"),
40 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):"),
39 ],
41 ],
40 )
42 )
41 def test_accept(text, suggestion, expected):
43 def test_accept(text, suggestion, expected):
42 event = make_event(text, len(text), suggestion)
44 event = make_event(text, len(text), suggestion)
43 buffer = event.current_buffer
45 buffer = event.current_buffer
44 buffer.insert_text = Mock()
46 buffer.insert_text = Mock()
45 accept(event)
47 accept(event)
46 assert buffer.insert_text.called
48 assert buffer.insert_text.called
47 assert buffer.insert_text.call_args[0] == (expected,)
49 assert buffer.insert_text.call_args[0] == (expected,)
48
50
49
51
50 @pytest.mark.parametrize(
52 @pytest.mark.parametrize(
51 "text, suggestion",
53 "text, suggestion",
52 [
54 [
53 ("", "def out(tag: str, n=50):"),
55 ("", "def out(tag: str, n=50):"),
54 ("def ", "out(tag: str, n=50):"),
56 ("def ", "out(tag: str, n=50):"),
55 ],
57 ],
56 )
58 )
57 def test_discard(text, suggestion):
59 def test_discard(text, suggestion):
58 event = make_event(text, len(text), suggestion)
60 event = make_event(text, len(text), suggestion)
59 buffer = event.current_buffer
61 buffer = event.current_buffer
60 buffer.insert_text = Mock()
62 buffer.insert_text = Mock()
61 discard(event)
63 discard(event)
62 assert not buffer.insert_text.called
64 assert not buffer.insert_text.called
63 assert buffer.suggestion is None
65 assert buffer.suggestion is None
64
66
65
67
66 @pytest.mark.parametrize(
68 @pytest.mark.parametrize(
67 "text, cursor, suggestion, called",
69 "text, cursor, suggestion, called",
68 [
70 [
69 ("123456", 6, "123456789", True),
71 ("123456", 6, "123456789", True),
70 ("123456", 3, "123456789", False),
72 ("123456", 3, "123456789", False),
71 ("123456 \n789", 6, "123456789", True),
73 ("123456 \n789", 6, "123456789", True),
72 ],
74 ],
73 )
75 )
74 def test_autosuggest_at_EOL(text, cursor, suggestion, called):
76 def test_autosuggest_at_EOL(text, cursor, suggestion, called):
75 """
77 """
76 test that autosuggest is only applied at end of line.
78 test that autosuggest is only applied at end of line.
77 """
79 """
78
80
79 event = make_event(text, cursor, suggestion)
81 event = make_event(text, cursor, suggestion)
80 event.current_buffer.insert_text = Mock()
82 event.current_buffer.insert_text = Mock()
81 accept_in_vi_insert_mode(event)
83 accept_in_vi_insert_mode(event)
82 if called:
84 if called:
83 event.current_buffer.insert_text.assert_called()
85 event.current_buffer.insert_text.assert_called()
84 else:
86 else:
85 event.current_buffer.insert_text.assert_not_called()
87 event.current_buffer.insert_text.assert_not_called()
86 # event.current_buffer.document.get_end_of_line_position.assert_called()
88 # event.current_buffer.document.get_end_of_line_position.assert_called()
87
89
88
90
89 @pytest.mark.parametrize(
91 @pytest.mark.parametrize(
90 "text, suggestion, expected",
92 "text, suggestion, expected",
91 [
93 [
92 ("", "def out(tag: str, n=50):", "def "),
94 ("", "def out(tag: str, n=50):", "def "),
93 ("d", "ef out(tag: str, n=50):", "ef "),
95 ("d", "ef out(tag: str, n=50):", "ef "),
94 ("de ", "f out(tag: str, n=50):", "f "),
96 ("de ", "f out(tag: str, n=50):", "f "),
95 ("def", " out(tag: str, n=50):", " "),
97 ("def", " out(tag: str, n=50):", " "),
96 ("def ", "out(tag: str, n=50):", "out("),
98 ("def ", "out(tag: str, n=50):", "out("),
97 ("def o", "ut(tag: str, n=50):", "ut("),
99 ("def o", "ut(tag: str, n=50):", "ut("),
98 ("def ou", "t(tag: str, n=50):", "t("),
100 ("def ou", "t(tag: str, n=50):", "t("),
99 ("def out", "(tag: str, n=50):", "("),
101 ("def out", "(tag: str, n=50):", "("),
100 ("def out(", "tag: str, n=50):", "tag: "),
102 ("def out(", "tag: str, n=50):", "tag: "),
101 ("def out(t", "ag: str, n=50):", "ag: "),
103 ("def out(t", "ag: str, n=50):", "ag: "),
102 ("def out(ta", "g: str, n=50):", "g: "),
104 ("def out(ta", "g: str, n=50):", "g: "),
103 ("def out(tag", ": str, n=50):", ": "),
105 ("def out(tag", ": str, n=50):", ": "),
104 ("def out(tag:", " str, n=50):", " "),
106 ("def out(tag:", " str, n=50):", " "),
105 ("def out(tag: ", "str, n=50):", "str, "),
107 ("def out(tag: ", "str, n=50):", "str, "),
106 ("def out(tag: s", "tr, n=50):", "tr, "),
108 ("def out(tag: s", "tr, n=50):", "tr, "),
107 ("def out(tag: st", "r, n=50):", "r, "),
109 ("def out(tag: st", "r, n=50):", "r, "),
108 ("def out(tag: str", ", n=50):", ", n"),
110 ("def out(tag: str", ", n=50):", ", n"),
109 ("def out(tag: str,", " n=50):", " n"),
111 ("def out(tag: str,", " n=50):", " n"),
110 ("def out(tag: str, ", "n=50):", "n="),
112 ("def out(tag: str, ", "n=50):", "n="),
111 ("def out(tag: str, n", "=50):", "="),
113 ("def out(tag: str, n", "=50):", "="),
112 ("def out(tag: str, n=", "50):", "50)"),
114 ("def out(tag: str, n=", "50):", "50)"),
113 ("def out(tag: str, n=5", "0):", "0)"),
115 ("def out(tag: str, n=5", "0):", "0)"),
114 ("def out(tag: str, n=50", "):", "):"),
116 ("def out(tag: str, n=50", "):", "):"),
115 ("def out(tag: str, n=50)", ":", ":"),
117 ("def out(tag: str, n=50)", ":", ":"),
116 ],
118 ],
117 )
119 )
118 def test_autosuggest_token(text, suggestion, expected):
120 def test_autosuggest_token(text, suggestion, expected):
119 event = make_event(text, len(text), suggestion)
121 event = make_event(text, len(text), suggestion)
120 event.current_buffer.insert_text = Mock()
122 event.current_buffer.insert_text = Mock()
121 accept_token(event)
123 accept_token(event)
122 assert event.current_buffer.insert_text.called
124 assert event.current_buffer.insert_text.called
123 assert event.current_buffer.insert_text.call_args[0] == (expected,)
125 assert event.current_buffer.insert_text.call_args[0] == (expected,)
124
126
125
127
126 @pytest.mark.parametrize(
128 @pytest.mark.parametrize(
127 "text, suggestion, expected",
129 "text, suggestion, expected",
128 [
130 [
129 ("", "def out(tag: str, n=50):", "d"),
131 ("", "def out(tag: str, n=50):", "d"),
130 ("d", "ef out(tag: str, n=50):", "e"),
132 ("d", "ef out(tag: str, n=50):", "e"),
131 ("de ", "f out(tag: str, n=50):", "f"),
133 ("de ", "f out(tag: str, n=50):", "f"),
132 ("def", " out(tag: str, n=50):", " "),
134 ("def", " out(tag: str, n=50):", " "),
133 ],
135 ],
134 )
136 )
135 def test_accept_character(text, suggestion, expected):
137 def test_accept_character(text, suggestion, expected):
136 event = make_event(text, len(text), suggestion)
138 event = make_event(text, len(text), suggestion)
137 event.current_buffer.insert_text = Mock()
139 event.current_buffer.insert_text = Mock()
138 accept_character(event)
140 accept_character(event)
139 assert event.current_buffer.insert_text.called
141 assert event.current_buffer.insert_text.called
140 assert event.current_buffer.insert_text.call_args[0] == (expected,)
142 assert event.current_buffer.insert_text.call_args[0] == (expected,)
141
143
142
144
143 @pytest.mark.parametrize(
145 @pytest.mark.parametrize(
144 "text, suggestion, expected",
146 "text, suggestion, expected",
145 [
147 [
146 ("", "def out(tag: str, n=50):", "def "),
148 ("", "def out(tag: str, n=50):", "def "),
147 ("d", "ef out(tag: str, n=50):", "ef "),
149 ("d", "ef out(tag: str, n=50):", "ef "),
148 ("de", "f out(tag: str, n=50):", "f "),
150 ("de", "f out(tag: str, n=50):", "f "),
149 ("def", " out(tag: str, n=50):", " "),
151 ("def", " out(tag: str, n=50):", " "),
150 # (this is why we also have accept_token)
152 # (this is why we also have accept_token)
151 ("def ", "out(tag: str, n=50):", "out(tag: "),
153 ("def ", "out(tag: str, n=50):", "out(tag: "),
152 ],
154 ],
153 )
155 )
154 def test_accept_word(text, suggestion, expected):
156 def test_accept_word(text, suggestion, expected):
155 event = make_event(text, len(text), suggestion)
157 event = make_event(text, len(text), suggestion)
156 event.current_buffer.insert_text = Mock()
158 event.current_buffer.insert_text = Mock()
157 accept_word(event)
159 accept_word(event)
158 assert event.current_buffer.insert_text.called
160 assert event.current_buffer.insert_text.called
159 assert event.current_buffer.insert_text.call_args[0] == (expected,)
161 assert event.current_buffer.insert_text.call_args[0] == (expected,)
160
162
161
163
162 @pytest.mark.parametrize(
164 @pytest.mark.parametrize(
163 "text, suggestion, expected, cursor",
165 "text, suggestion, expected, cursor",
164 [
166 [
165 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):", 0),
167 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):", 0),
166 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):", 4),
168 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):", 4),
167 ],
169 ],
168 )
170 )
169 def test_accept_and_keep_cursor(text, suggestion, expected, cursor):
171 def test_accept_and_keep_cursor(text, suggestion, expected, cursor):
170 event = make_event(text, cursor, suggestion)
172 event = make_event(text, cursor, suggestion)
171 buffer = event.current_buffer
173 buffer = event.current_buffer
172 buffer.insert_text = Mock()
174 buffer.insert_text = Mock()
173 accept_and_keep_cursor(event)
175 accept_and_keep_cursor(event)
174 assert buffer.insert_text.called
176 assert buffer.insert_text.called
175 assert buffer.insert_text.call_args[0] == (expected,)
177 assert buffer.insert_text.call_args[0] == (expected,)
176 assert buffer.cursor_position == cursor
178 assert buffer.cursor_position == cursor
177
179
178
180
179 def test_autosuggest_token_empty():
181 def test_autosuggest_token_empty():
180 full = "def out(tag: str, n=50):"
182 full = "def out(tag: str, n=50):"
181 event = make_event(full, len(full), "")
183 event = make_event(full, len(full), "")
182 event.current_buffer.insert_text = Mock()
184 event.current_buffer.insert_text = Mock()
183
185
184 with patch(
186 with patch(
185 "prompt_toolkit.key_binding.bindings.named_commands.forward_word"
187 "prompt_toolkit.key_binding.bindings.named_commands.forward_word"
186 ) as forward_word:
188 ) as forward_word:
187 accept_token(event)
189 accept_token(event)
188 assert not event.current_buffer.insert_text.called
190 assert not event.current_buffer.insert_text.called
189 assert forward_word.called
191 assert forward_word.called
190
192
191
193
192 def test_other_providers():
194 def test_other_providers():
193 """Ensure that swapping autosuggestions does not break with other providers"""
195 """Ensure that swapping autosuggestions does not break with other providers"""
194 provider = AutoSuggestFromHistory()
196 provider = AutoSuggestFromHistory()
195 up = swap_autosuggestion_up(provider)
197 ip = get_ipython()
196 down = swap_autosuggestion_down(provider)
198 ip.auto_suggest = provider
197 event = Mock()
199 event = Mock()
198 event.current_buffer = Buffer()
200 event.current_buffer = Buffer()
199 assert up(event) is None
201 assert swap_autosuggestion_up(event) is None
200 assert down(event) is None
202 assert swap_autosuggestion_down(event) is None
201
203
202
204
203 async def test_navigable_provider():
205 async def test_navigable_provider():
204 provider = NavigableAutoSuggestFromHistory()
206 provider = NavigableAutoSuggestFromHistory()
205 history = InMemoryHistory(history_strings=["very_a", "very", "very_b", "very_c"])
207 history = InMemoryHistory(history_strings=["very_a", "very", "very_b", "very_c"])
206 buffer = Buffer(history=history)
208 buffer = Buffer(history=history)
209 ip = get_ipython()
210 ip.auto_suggest = provider
207
211
208 async for _ in history.load():
212 async for _ in history.load():
209 pass
213 pass
210
214
211 buffer.cursor_position = 5
215 buffer.cursor_position = 5
212 buffer.text = "very"
216 buffer.text = "very"
213
217
214 up = swap_autosuggestion_up(provider)
218 up = swap_autosuggestion_up
215 down = swap_autosuggestion_down(provider)
219 down = swap_autosuggestion_down
216
220
217 event = Mock()
221 event = Mock()
218 event.current_buffer = buffer
222 event.current_buffer = buffer
219
223
220 def get_suggestion():
224 def get_suggestion():
221 suggestion = provider.get_suggestion(buffer, buffer.document)
225 suggestion = provider.get_suggestion(buffer, buffer.document)
222 buffer.suggestion = suggestion
226 buffer.suggestion = suggestion
223 return suggestion
227 return suggestion
224
228
225 assert get_suggestion().text == "_c"
229 assert get_suggestion().text == "_c"
226
230
227 # should go up
231 # should go up
228 up(event)
232 up(event)
229 assert get_suggestion().text == "_b"
233 assert get_suggestion().text == "_b"
230
234
231 # should skip over 'very' which is identical to buffer content
235 # should skip over 'very' which is identical to buffer content
232 up(event)
236 up(event)
233 assert get_suggestion().text == "_a"
237 assert get_suggestion().text == "_a"
234
238
235 # should cycle back to beginning
239 # should cycle back to beginning
236 up(event)
240 up(event)
237 assert get_suggestion().text == "_c"
241 assert get_suggestion().text == "_c"
238
242
239 # should cycle back through end boundary
243 # should cycle back through end boundary
240 down(event)
244 down(event)
241 assert get_suggestion().text == "_a"
245 assert get_suggestion().text == "_a"
242
246
243 down(event)
247 down(event)
244 assert get_suggestion().text == "_b"
248 assert get_suggestion().text == "_b"
245
249
246 down(event)
250 down(event)
247 assert get_suggestion().text == "_c"
251 assert get_suggestion().text == "_c"
248
252
249 down(event)
253 down(event)
250 assert get_suggestion().text == "_a"
254 assert get_suggestion().text == "_a"
251
255
252
256
253 async def test_navigable_provider_multiline_entries():
257 async def test_navigable_provider_multiline_entries():
254 provider = NavigableAutoSuggestFromHistory()
258 provider = NavigableAutoSuggestFromHistory()
255 history = InMemoryHistory(history_strings=["very_a\nvery_b", "very_c"])
259 history = InMemoryHistory(history_strings=["very_a\nvery_b", "very_c"])
256 buffer = Buffer(history=history)
260 buffer = Buffer(history=history)
261 ip = get_ipython()
262 ip.auto_suggest = provider
257
263
258 async for _ in history.load():
264 async for _ in history.load():
259 pass
265 pass
260
266
261 buffer.cursor_position = 5
267 buffer.cursor_position = 5
262 buffer.text = "very"
268 buffer.text = "very"
263 up = swap_autosuggestion_up(provider)
269 up = swap_autosuggestion_up
264 down = swap_autosuggestion_down(provider)
270 down = swap_autosuggestion_down
265
271
266 event = Mock()
272 event = Mock()
267 event.current_buffer = buffer
273 event.current_buffer = buffer
268
274
269 def get_suggestion():
275 def get_suggestion():
270 suggestion = provider.get_suggestion(buffer, buffer.document)
276 suggestion = provider.get_suggestion(buffer, buffer.document)
271 buffer.suggestion = suggestion
277 buffer.suggestion = suggestion
272 return suggestion
278 return suggestion
273
279
274 assert get_suggestion().text == "_c"
280 assert get_suggestion().text == "_c"
275
281
276 up(event)
282 up(event)
277 assert get_suggestion().text == "_b"
283 assert get_suggestion().text == "_b"
278
284
279 up(event)
285 up(event)
280 assert get_suggestion().text == "_a"
286 assert get_suggestion().text == "_a"
281
287
282 down(event)
288 down(event)
283 assert get_suggestion().text == "_b"
289 assert get_suggestion().text == "_b"
284
290
285 down(event)
291 down(event)
286 assert get_suggestion().text == "_c"
292 assert get_suggestion().text == "_c"
287
293
288
294
289 def create_session_mock():
295 def create_session_mock():
290 session = Mock()
296 session = Mock()
291 session.default_buffer = Buffer()
297 session.default_buffer = Buffer()
292 return session
298 return session
293
299
294
300
295 def test_navigable_provider_connection():
301 def test_navigable_provider_connection():
296 provider = NavigableAutoSuggestFromHistory()
302 provider = NavigableAutoSuggestFromHistory()
297 provider.skip_lines = 1
303 provider.skip_lines = 1
298
304
299 session_1 = create_session_mock()
305 session_1 = create_session_mock()
300 provider.connect(session_1)
306 provider.connect(session_1)
301
307
302 assert provider.skip_lines == 1
308 assert provider.skip_lines == 1
303 session_1.default_buffer.on_text_insert.fire()
309 session_1.default_buffer.on_text_insert.fire()
304 assert provider.skip_lines == 0
310 assert provider.skip_lines == 0
305
311
306 session_2 = create_session_mock()
312 session_2 = create_session_mock()
307 provider.connect(session_2)
313 provider.connect(session_2)
308 provider.skip_lines = 2
314 provider.skip_lines = 2
309
315
310 assert provider.skip_lines == 2
316 assert provider.skip_lines == 2
311 session_2.default_buffer.on_text_insert.fire()
317 session_2.default_buffer.on_text_insert.fire()
312 assert provider.skip_lines == 0
318 assert provider.skip_lines == 0
313
319
314 provider.skip_lines = 3
320 provider.skip_lines = 3
315 provider.disconnect()
321 provider.disconnect()
316 session_1.default_buffer.on_text_insert.fire()
322 session_1.default_buffer.on_text_insert.fire()
317 session_2.default_buffer.on_text_insert.fire()
323 session_2.default_buffer.on_text_insert.fire()
318 assert provider.skip_lines == 3
324 assert provider.skip_lines == 3
325
326
327 @pytest.fixture
328 def ipython_with_prompt():
329 ip = get_ipython()
330 ip.pt_app = Mock()
331 ip.pt_app.key_bindings = create_ipython_shortcuts(ip)
332 try:
333 yield ip
334 finally:
335 ip.pt_app = None
336
337
338 def find_bindings_by_command(command):
339 ip = get_ipython()
340 return [
341 binding
342 for binding in ip.pt_app.key_bindings.bindings
343 if binding.handler == command
344 ]
345
346
347 def test_modify_unique_shortcut(ipython_with_prompt):
348 original = find_bindings_by_command(accept_token)
349 assert len(original) == 1
350
351 ipython_with_prompt.shortcuts = [
352 {"command": "IPython:auto_suggest.accept_token", "new_keys": ["a", "b", "c"]}
353 ]
354 matched = find_bindings_by_command(accept_token)
355 assert len(matched) == 1
356 assert list(matched[0].keys) == ["a", "b", "c"]
357 assert list(matched[0].keys) != list(original[0].keys)
358 assert matched[0].filter == original[0].filter
359
360 ipython_with_prompt.shortcuts = [
361 {"command": "IPython:auto_suggest.accept_token", "new_filter": "always"}
362 ]
363 matched = find_bindings_by_command(accept_token)
364 assert len(matched) == 1
365 assert list(matched[0].keys) != ["a", "b", "c"]
366 assert list(matched[0].keys) == list(original[0].keys)
367 assert matched[0].filter != original[0].filter
368
369
370 def test_disable_shortcut(ipython_with_prompt):
371 matched = find_bindings_by_command(accept_token)
372 assert len(matched) == 1
373
374 ipython_with_prompt.shortcuts = [
375 {"command": "IPython:auto_suggest.accept_token", "new_keys": []}
376 ]
377 matched = find_bindings_by_command(accept_token)
378 assert len(matched) == 0
379
380 ipython_with_prompt.shortcuts = []
381 matched = find_bindings_by_command(accept_token)
382 assert len(matched) == 1
383
384
385 def test_modify_shortcut_with_filters(ipython_with_prompt):
386 matched = find_bindings_by_command(skip_over)
387 matched_keys = {m.keys[0] for m in matched}
388 assert matched_keys == {")", "]", "}", "'", '"'}
389
390 with pytest.raises(ValueError, match="Multiple shortcuts matching"):
391 ipython_with_prompt.shortcuts = [
392 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"]}
393 ]
394
395 ipython_with_prompt.shortcuts = [
396 {
397 "command": "IPython:auto_match.skip_over",
398 "new_keys": ["x"],
399 "match_filter": "focused_insert & auto_match & followed_by_single_quote",
400 }
401 ]
402 matched = find_bindings_by_command(skip_over)
403 matched_keys = {m.keys[0] for m in matched}
404 assert matched_keys == {")", "]", "}", "x", '"'}
405
406
407 def example_command():
408 pass
409
410
411 def test_add_shortcut_for_new_command(ipython_with_prompt):
412 matched = find_bindings_by_command(example_command)
413 assert len(matched) == 0
414
415 with pytest.raises(ValueError, match="example_command is not a known"):
416 ipython_with_prompt.shortcuts = [
417 {"command": "example_command", "new_keys": ["x"]}
418 ]
419 matched = find_bindings_by_command(example_command)
420 assert len(matched) == 0
421
422
423 def test_modify_shortcut_failure(ipython_with_prompt):
424 with pytest.raises(ValueError, match="No shortcuts matching"):
425 ipython_with_prompt.shortcuts = [
426 {
427 "command": "IPython:auto_match.skip_over",
428 "match_keys": ["x"],
429 "new_keys": ["y"],
430 }
431 ]
432
433
434 def test_add_shortcut_for_existing_command(ipython_with_prompt):
435 matched = find_bindings_by_command(skip_over)
436 assert len(matched) == 5
437
438 with pytest.raises(ValueError, match="Cannot add a shortcut without keys"):
439 ipython_with_prompt.shortcuts = [
440 {"command": "IPython:auto_match.skip_over", "new_keys": [], "create": True}
441 ]
442
443 ipython_with_prompt.shortcuts = [
444 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"], "create": True}
445 ]
446 matched = find_bindings_by_command(skip_over)
447 assert len(matched) == 6
448
449 ipython_with_prompt.shortcuts = []
450 matched = find_bindings_by_command(skip_over)
451 assert len(matched) == 5
452
453
454 def test_setting_shortcuts_before_pt_app_init():
455 ipython = get_ipython()
456 assert ipython.pt_app is None
457 shortcuts = [
458 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"], "create": True}
459 ]
460 ipython.shortcuts = shortcuts
461 assert ipython.shortcuts == shortcuts
@@ -1,221 +1,221 b''
1 from dataclasses import dataclass
1 from dataclasses import dataclass
2 from inspect import getsource
2 from inspect import getsource
3 from pathlib import Path
3 from pathlib import Path
4 from typing import cast, Callable, List, Union
4 from typing import cast, List, Union
5 from html import escape as html_escape
5 from html import escape as html_escape
6 import re
6 import re
7
7
8 from prompt_toolkit.keys import KEY_ALIASES
8 from prompt_toolkit.keys import KEY_ALIASES
9 from prompt_toolkit.key_binding import KeyBindingsBase
9 from prompt_toolkit.key_binding import KeyBindingsBase
10 from prompt_toolkit.filters import Filter, Condition
10 from prompt_toolkit.filters import Filter, Condition
11 from prompt_toolkit.shortcuts import PromptSession
11 from prompt_toolkit.shortcuts import PromptSession
12
12
13 from IPython.terminal.shortcuts import create_ipython_shortcuts
13 from IPython.terminal.shortcuts import create_ipython_shortcuts, create_identifier
14 from IPython.terminal.shortcuts.filters import KEYBINDING_FILTERS
14
15
15
16
16 @dataclass
17 @dataclass
17 class Shortcut:
18 class Shortcut:
18 #: a sequence of keys (each element on the list corresponds to pressing one or more keys)
19 #: a sequence of keys (each element on the list corresponds to pressing one or more keys)
19 keys_sequence: list[str]
20 keys_sequence: list[str]
20 filter: str
21 filter: str
21
22
22
23
23 @dataclass
24 @dataclass
24 class Handler:
25 class Handler:
25 description: str
26 description: str
26 identifier: str
27 identifier: str
27
28
28
29
29 @dataclass
30 @dataclass
30 class Binding:
31 class Binding:
31 handler: Handler
32 handler: Handler
32 shortcut: Shortcut
33 shortcut: Shortcut
33
34
34
35
35 class _NestedFilter(Filter):
36 class _NestedFilter(Filter):
36 """Protocol reflecting non-public prompt_toolkit's `_AndList` and `_OrList`."""
37 """Protocol reflecting non-public prompt_toolkit's `_AndList` and `_OrList`."""
37
38
38 filters: List[Filter]
39 filters: List[Filter]
39
40
40
41
41 class _Invert(Filter):
42 class _Invert(Filter):
42 """Protocol reflecting non-public prompt_toolkit's `_Invert`."""
43 """Protocol reflecting non-public prompt_toolkit's `_Invert`."""
43
44
44 filter: Filter
45 filter: Filter
45
46
46
47
47 conjunctions_labels = {"_AndList": "and", "_OrList": "or"}
48 conjunctions_labels = {"_AndList": "&", "_OrList": "|"}
48
49
49 ATOMIC_CLASSES = {"Never", "Always", "Condition"}
50 ATOMIC_CLASSES = {"Never", "Always", "Condition"}
50
51
51
52
53 HUMAN_NAMES_FOR_FILTERS = {
54 filter_: name for name, filter_ in KEYBINDING_FILTERS.items()
55 }
56
57
52 def format_filter(
58 def format_filter(
53 filter_: Union[Filter, _NestedFilter, Condition, _Invert],
59 filter_: Union[Filter, _NestedFilter, Condition, _Invert],
54 is_top_level=True,
60 is_top_level=True,
55 skip=None,
61 skip=None,
56 ) -> str:
62 ) -> str:
57 """Create easily readable description of the filter."""
63 """Create easily readable description of the filter."""
58 s = filter_.__class__.__name__
64 s = filter_.__class__.__name__
59 if s == "Condition":
65 if s == "Condition":
60 func = cast(Condition, filter_).func
66 func = cast(Condition, filter_).func
67 if filter_ in HUMAN_NAMES_FOR_FILTERS:
68 return HUMAN_NAMES_FOR_FILTERS[filter_]
61 name = func.__name__
69 name = func.__name__
62 if name == "<lambda>":
70 if name == "<lambda>":
63 source = getsource(func)
71 source = getsource(func)
64 return source.split("=")[0].strip()
72 return source.split("=")[0].strip()
65 return func.__name__
73 return func.__name__
66 elif s == "_Invert":
74 elif s == "_Invert":
67 operand = cast(_Invert, filter_).filter
75 operand = cast(_Invert, filter_).filter
68 if operand.__class__.__name__ in ATOMIC_CLASSES:
76 if operand.__class__.__name__ in ATOMIC_CLASSES:
69 return f"not {format_filter(operand, is_top_level=False)}"
77 return f"~{format_filter(operand, is_top_level=False)}"
70 return f"not ({format_filter(operand, is_top_level=False)})"
78 return f"~({format_filter(operand, is_top_level=False)})"
71 elif s in conjunctions_labels:
79 elif s in conjunctions_labels:
72 filters = cast(_NestedFilter, filter_).filters
80 filters = cast(_NestedFilter, filter_).filters
81 if filter_ in HUMAN_NAMES_FOR_FILTERS:
82 return HUMAN_NAMES_FOR_FILTERS[filter_]
73 conjunction = conjunctions_labels[s]
83 conjunction = conjunctions_labels[s]
74 glue = f" {conjunction} "
84 glue = f" {conjunction} "
75 result = glue.join(format_filter(x, is_top_level=False) for x in filters)
85 result = glue.join(format_filter(x, is_top_level=False) for x in filters)
76 if len(filters) > 1 and not is_top_level:
86 if len(filters) > 1 and not is_top_level:
77 result = f"({result})"
87 result = f"({result})"
78 return result
88 return result
79 elif s in ["Never", "Always"]:
89 elif s in ["Never", "Always"]:
80 return s.lower()
90 return s.lower()
81 else:
91 else:
82 raise ValueError(f"Unknown filter type: {filter_}")
92 raise ValueError(f"Unknown filter type: {filter_}")
83
93
84
94
85 def sentencize(s) -> str:
95 def sentencize(s) -> str:
86 """Extract first sentence"""
96 """Extract first sentence"""
87 s = re.split(r"\.\W", s.replace("\n", " ").strip())
97 s = re.split(r"\.\W", s.replace("\n", " ").strip())
88 s = s[0] if len(s) else ""
98 s = s[0] if len(s) else ""
89 if not s.endswith("."):
99 if not s.endswith("."):
90 s += "."
100 s += "."
91 try:
101 try:
92 return " ".join(s.split())
102 return " ".join(s.split())
93 except AttributeError:
103 except AttributeError:
94 return s
104 return s
95
105
96
106
97 class _DummyTerminal:
107 class _DummyTerminal:
98 """Used as a buffer to get prompt_toolkit bindings
108 """Used as a buffer to get prompt_toolkit bindings
99 """
109 """
100 handle_return = None
110 handle_return = None
101 input_transformer_manager = None
111 input_transformer_manager = None
102 display_completions = None
112 display_completions = None
103 editing_mode = "emacs"
113 editing_mode = "emacs"
104 auto_suggest = None
114 auto_suggest = None
105
115
106
116
107 def create_identifier(handler: Callable):
108 parts = handler.__module__.split(".")
109 name = handler.__name__
110 package = parts[0]
111 if len(parts) > 1:
112 final_module = parts[-1]
113 return f"{package}:{final_module}.{name}"
114 else:
115 return f"{package}:{name}"
116
117
118 def bindings_from_prompt_toolkit(prompt_bindings: KeyBindingsBase) -> List[Binding]:
117 def bindings_from_prompt_toolkit(prompt_bindings: KeyBindingsBase) -> List[Binding]:
119 """Collect bindings to a simple format that does not depend on prompt-toolkit internals"""
118 """Collect bindings to a simple format that does not depend on prompt-toolkit internals"""
120 bindings: List[Binding] = []
119 bindings: List[Binding] = []
121
120
122 for kb in prompt_bindings.bindings:
121 for kb in prompt_bindings.bindings:
123 bindings.append(
122 bindings.append(
124 Binding(
123 Binding(
125 handler=Handler(
124 handler=Handler(
126 description=kb.handler.__doc__ or "",
125 description=kb.handler.__doc__ or "",
127 identifier=create_identifier(kb.handler),
126 identifier=create_identifier(kb.handler),
128 ),
127 ),
129 shortcut=Shortcut(
128 shortcut=Shortcut(
130 keys_sequence=[
129 keys_sequence=[
131 str(k.value) if hasattr(k, "value") else k for k in kb.keys
130 str(k.value) if hasattr(k, "value") else k for k in kb.keys
132 ],
131 ],
133 filter=format_filter(kb.filter, skip={"has_focus_filter"}),
132 filter=format_filter(kb.filter, skip={"has_focus_filter"}),
134 ),
133 ),
135 )
134 )
136 )
135 )
137 return bindings
136 return bindings
138
137
139
138
140 INDISTINGUISHABLE_KEYS = {**KEY_ALIASES, **{v: k for k, v in KEY_ALIASES.items()}}
139 INDISTINGUISHABLE_KEYS = {**KEY_ALIASES, **{v: k for k, v in KEY_ALIASES.items()}}
141
140
142
141
143 def format_prompt_keys(keys: str, add_alternatives=True) -> str:
142 def format_prompt_keys(keys: str, add_alternatives=True) -> str:
144 """Format prompt toolkit key with modifier into an RST representation."""
143 """Format prompt toolkit key with modifier into an RST representation."""
145
144
146 def to_rst(key):
145 def to_rst(key):
147 escaped = key.replace("\\", "\\\\")
146 escaped = key.replace("\\", "\\\\")
148 return f":kbd:`{escaped}`"
147 return f":kbd:`{escaped}`"
149
148
150 keys_to_press: list[str]
149 keys_to_press: list[str]
151
150
152 prefixes = {
151 prefixes = {
153 "c-s-": [to_rst("ctrl"), to_rst("shift")],
152 "c-s-": [to_rst("ctrl"), to_rst("shift")],
154 "s-c-": [to_rst("ctrl"), to_rst("shift")],
153 "s-c-": [to_rst("ctrl"), to_rst("shift")],
155 "c-": [to_rst("ctrl")],
154 "c-": [to_rst("ctrl")],
156 "s-": [to_rst("shift")],
155 "s-": [to_rst("shift")],
157 }
156 }
158
157
159 for prefix, modifiers in prefixes.items():
158 for prefix, modifiers in prefixes.items():
160 if keys.startswith(prefix):
159 if keys.startswith(prefix):
161 remainder = keys[len(prefix) :]
160 remainder = keys[len(prefix) :]
162 keys_to_press = [*modifiers, to_rst(remainder)]
161 keys_to_press = [*modifiers, to_rst(remainder)]
163 break
162 break
164 else:
163 else:
165 keys_to_press = [to_rst(keys)]
164 keys_to_press = [to_rst(keys)]
166
165
167 result = " + ".join(keys_to_press)
166 result = " + ".join(keys_to_press)
168
167
169 if keys in INDISTINGUISHABLE_KEYS and add_alternatives:
168 if keys in INDISTINGUISHABLE_KEYS and add_alternatives:
170 alternative = INDISTINGUISHABLE_KEYS[keys]
169 alternative = INDISTINGUISHABLE_KEYS[keys]
171
170
172 result = (
171 result = (
173 result
172 result
174 + " (or "
173 + " (or "
175 + format_prompt_keys(alternative, add_alternatives=False)
174 + format_prompt_keys(alternative, add_alternatives=False)
176 + ")"
175 + ")"
177 )
176 )
178
177
179 return result
178 return result
180
179
180
181 if __name__ == '__main__':
181 if __name__ == '__main__':
182 here = Path(__file__).parent
182 here = Path(__file__).parent
183 dest = here / "source" / "config" / "shortcuts"
183 dest = here / "source" / "config" / "shortcuts"
184
184
185 ipy_bindings = create_ipython_shortcuts(_DummyTerminal(), for_all_platforms=True)
185 ipy_bindings = create_ipython_shortcuts(_DummyTerminal())
186
186
187 session = PromptSession(key_bindings=ipy_bindings)
187 session = PromptSession(key_bindings=ipy_bindings)
188 prompt_bindings = session.app.key_bindings
188 prompt_bindings = session.app.key_bindings
189
189
190 assert prompt_bindings
190 assert prompt_bindings
191 # Ensure that we collected the default shortcuts
191 # Ensure that we collected the default shortcuts
192 assert len(prompt_bindings.bindings) > len(ipy_bindings.bindings)
192 assert len(prompt_bindings.bindings) > len(ipy_bindings.bindings)
193
193
194 bindings = bindings_from_prompt_toolkit(prompt_bindings)
194 bindings = bindings_from_prompt_toolkit(prompt_bindings)
195
195
196 def sort_key(binding: Binding):
196 def sort_key(binding: Binding):
197 return binding.handler.identifier, binding.shortcut.filter
197 return binding.handler.identifier, binding.shortcut.filter
198
198
199 filters = []
199 filters = []
200 with (dest / "table.tsv").open("w", encoding="utf-8") as csv:
200 with (dest / "table.tsv").open("w", encoding="utf-8") as csv:
201 for binding in sorted(bindings, key=sort_key):
201 for binding in sorted(bindings, key=sort_key):
202 sequence = ", ".join(
202 sequence = ", ".join(
203 [format_prompt_keys(keys) for keys in binding.shortcut.keys_sequence]
203 [format_prompt_keys(keys) for keys in binding.shortcut.keys_sequence]
204 )
204 )
205 if binding.shortcut.filter == "always":
205 if binding.shortcut.filter == "always":
206 condition_label = "-"
206 condition_label = "-"
207 else:
207 else:
208 # we cannot fit all the columns as the filters got too complex over time
208 # we cannot fit all the columns as the filters got too complex over time
209 condition_label = "ⓘ"
209 condition_label = "ⓘ"
210
210
211 csv.write(
211 csv.write(
212 "\t".join(
212 "\t".join(
213 [
213 [
214 sequence,
214 sequence,
215 sentencize(binding.handler.description)
215 sentencize(binding.handler.description)
216 + f" :raw-html:`<br>` `{binding.handler.identifier}`",
216 + f" :raw-html:`<br>` `{binding.handler.identifier}`",
217 f':raw-html:`<span title="{html_escape(binding.shortcut.filter)}" style="cursor: help">{condition_label}</span>`',
217 f':raw-html:`<span title="{html_escape(binding.shortcut.filter)}" style="cursor: help">{condition_label}</span>`',
218 ]
218 ]
219 )
219 )
220 + "\n"
220 + "\n"
221 )
221 )
@@ -1,18 +1,19 b''
1 name: ipython_docs
1 name: ipython_docs
2 channels:
2 channels:
3 - conda-forge
3 - conda-forge
4 - defaults
4 - defaults
5 dependencies:
5 dependencies:
6 - python=3.10
6 - python=3.10
7 - setuptools
7 - setuptools
8 - sphinx>=4.2
8 - sphinx>=4.2
9 - sphinx_rtd_theme
9 - sphinx_rtd_theme
10 - numpy
10 - numpy
11 - testpath
11 - testpath
12 - matplotlib
12 - matplotlib
13 - pip
13 - pip
14 - pip:
14 - pip:
15 - docrepr
15 - docrepr
16 - prompt_toolkit
16 - prompt_toolkit
17 - ipykernel
17 - ipykernel
18 - stack_data
18 - stack_data
19 - -e ..
@@ -1,313 +1,323 b''
1 =======================
1 =======================
2 Specific config details
2 Specific config details
3 =======================
3 =======================
4
4
5 .. _custom_prompts:
5 .. _custom_prompts:
6
6
7 Custom Prompts
7 Custom Prompts
8 ==============
8 ==============
9
9
10 .. versionchanged:: 5.0
10 .. versionchanged:: 5.0
11
11
12 From IPython 5, prompts are produced as a list of Pygments tokens, which are
12 From IPython 5, prompts are produced as a list of Pygments tokens, which are
13 tuples of (token_type, text). You can customise prompts by writing a method
13 tuples of (token_type, text). You can customise prompts by writing a method
14 which generates a list of tokens.
14 which generates a list of tokens.
15
15
16 There are four kinds of prompt:
16 There are four kinds of prompt:
17
17
18 * The **in** prompt is shown before the first line of input
18 * The **in** prompt is shown before the first line of input
19 (default like ``In [1]:``).
19 (default like ``In [1]:``).
20 * The **continuation** prompt is shown before further lines of input
20 * The **continuation** prompt is shown before further lines of input
21 (default like ``...:``).
21 (default like ``...:``).
22 * The **rewrite** prompt is shown to highlight how special syntax has been
22 * The **rewrite** prompt is shown to highlight how special syntax has been
23 interpreted (default like ``----->``).
23 interpreted (default like ``----->``).
24 * The **out** prompt is shown before the result from evaluating the input
24 * The **out** prompt is shown before the result from evaluating the input
25 (default like ``Out[1]:``).
25 (default like ``Out[1]:``).
26
26
27 Custom prompts are supplied together as a class. If you want to customise only
27 Custom prompts are supplied together as a class. If you want to customise only
28 some of the prompts, inherit from :class:`IPython.terminal.prompts.Prompts`,
28 some of the prompts, inherit from :class:`IPython.terminal.prompts.Prompts`,
29 which defines the defaults. The required interface is like this:
29 which defines the defaults. The required interface is like this:
30
30
31 .. class:: MyPrompts(shell)
31 .. class:: MyPrompts(shell)
32
32
33 Prompt style definition. *shell* is a reference to the
33 Prompt style definition. *shell* is a reference to the
34 :class:`~.TerminalInteractiveShell` instance.
34 :class:`~.TerminalInteractiveShell` instance.
35
35
36 .. method:: in_prompt_tokens(cli=None)
36 .. method:: in_prompt_tokens(cli=None)
37 continuation_prompt_tokens(self, cli=None, width=None)
37 continuation_prompt_tokens(self, cli=None, width=None)
38 rewrite_prompt_tokens()
38 rewrite_prompt_tokens()
39 out_prompt_tokens()
39 out_prompt_tokens()
40
40
41 Return the respective prompts as lists of ``(token_type, text)`` tuples.
41 Return the respective prompts as lists of ``(token_type, text)`` tuples.
42
42
43 For continuation prompts, *width* is an integer representing the width of
43 For continuation prompts, *width* is an integer representing the width of
44 the prompt area in terminal columns.
44 the prompt area in terminal columns.
45
45
46 *cli*, where used, is the prompt_toolkit ``CommandLineInterface`` instance.
46 *cli*, where used, is the prompt_toolkit ``CommandLineInterface`` instance.
47 This is mainly for compatibility with the API prompt_toolkit expects.
47 This is mainly for compatibility with the API prompt_toolkit expects.
48
48
49 Here is an example Prompt class that will show the current working directory
49 Here is an example Prompt class that will show the current working directory
50 in the input prompt:
50 in the input prompt:
51
51
52 .. code-block:: python
52 .. code-block:: python
53
53
54 from IPython.terminal.prompts import Prompts, Token
54 from IPython.terminal.prompts import Prompts, Token
55 import os
55 import os
56
56
57 class MyPrompt(Prompts):
57 class MyPrompt(Prompts):
58 def in_prompt_tokens(self, cli=None):
58 def in_prompt_tokens(self, cli=None):
59 return [(Token, os.getcwd()),
59 return [(Token, os.getcwd()),
60 (Token.Prompt, ' >>>')]
60 (Token.Prompt, ' >>>')]
61
61
62 To set the new prompt, assign it to the ``prompts`` attribute of the IPython
62 To set the new prompt, assign it to the ``prompts`` attribute of the IPython
63 shell:
63 shell:
64
64
65 .. code-block:: python
65 .. code-block:: python
66
66
67 In [2]: ip = get_ipython()
67 In [2]: ip = get_ipython()
68 ...: ip.prompts = MyPrompt(ip)
68 ...: ip.prompts = MyPrompt(ip)
69
69
70 /home/bob >>> # it works
70 /home/bob >>> # it works
71
71
72 See ``IPython/example/utils/cwd_prompt.py`` for an example of how to write
72 See ``IPython/example/utils/cwd_prompt.py`` for an example of how to write
73 extensions to customise prompts.
73 extensions to customise prompts.
74
74
75 Inside IPython or in a startup script, you can use a custom prompts class
75 Inside IPython or in a startup script, you can use a custom prompts class
76 by setting ``get_ipython().prompts`` to an *instance* of the class.
76 by setting ``get_ipython().prompts`` to an *instance* of the class.
77 In configuration, ``TerminalInteractiveShell.prompts_class`` may be set to
77 In configuration, ``TerminalInteractiveShell.prompts_class`` may be set to
78 either the class object, or a string of its full importable name.
78 either the class object, or a string of its full importable name.
79
79
80 To include invisible terminal control sequences in a prompt, use
80 To include invisible terminal control sequences in a prompt, use
81 ``Token.ZeroWidthEscape`` as the token type. Tokens with this type are ignored
81 ``Token.ZeroWidthEscape`` as the token type. Tokens with this type are ignored
82 when calculating the width.
82 when calculating the width.
83
83
84 Colours in the prompt are determined by the token types and the highlighting
84 Colours in the prompt are determined by the token types and the highlighting
85 style; see below for more details. The tokens used in the default prompts are
85 style; see below for more details. The tokens used in the default prompts are
86 ``Prompt``, ``PromptNum``, ``OutPrompt`` and ``OutPromptNum``.
86 ``Prompt``, ``PromptNum``, ``OutPrompt`` and ``OutPromptNum``.
87
87
88 .. _termcolour:
88 .. _termcolour:
89
89
90 Terminal Colors
90 Terminal Colors
91 ===============
91 ===============
92
92
93 .. versionchanged:: 5.0
93 .. versionchanged:: 5.0
94
94
95 There are two main configuration options controlling colours.
95 There are two main configuration options controlling colours.
96
96
97 ``InteractiveShell.colors`` sets the colour of tracebacks and object info (the
97 ``InteractiveShell.colors`` sets the colour of tracebacks and object info (the
98 output from e.g. ``zip?``). It may also affect other things if the option below
98 output from e.g. ``zip?``). It may also affect other things if the option below
99 is set to ``'legacy'``. It has four case-insensitive values:
99 is set to ``'legacy'``. It has four case-insensitive values:
100 ``'nocolor', 'neutral', 'linux', 'lightbg'``. The default is *neutral*, which
100 ``'nocolor', 'neutral', 'linux', 'lightbg'``. The default is *neutral*, which
101 should be legible on either dark or light terminal backgrounds. *linux* is
101 should be legible on either dark or light terminal backgrounds. *linux* is
102 optimised for dark backgrounds and *lightbg* for light ones.
102 optimised for dark backgrounds and *lightbg* for light ones.
103
103
104 ``TerminalInteractiveShell.highlighting_style`` determines prompt colours and
104 ``TerminalInteractiveShell.highlighting_style`` determines prompt colours and
105 syntax highlighting. It takes the name (as a string) or class (as a subclass of
105 syntax highlighting. It takes the name (as a string) or class (as a subclass of
106 ``pygments.style.Style``) of a Pygments style, or the special value ``'legacy'``
106 ``pygments.style.Style``) of a Pygments style, or the special value ``'legacy'``
107 to pick a style in accordance with ``InteractiveShell.colors``.
107 to pick a style in accordance with ``InteractiveShell.colors``.
108
108
109 You can see the Pygments styles available on your system by running::
109 You can see the Pygments styles available on your system by running::
110
110
111 import pygments
111 import pygments
112 list(pygments.styles.get_all_styles())
112 list(pygments.styles.get_all_styles())
113
113
114 Additionally, ``TerminalInteractiveShell.highlighting_style_overrides`` can override
114 Additionally, ``TerminalInteractiveShell.highlighting_style_overrides`` can override
115 specific styles in the highlighting. It should be a dictionary mapping Pygments
115 specific styles in the highlighting. It should be a dictionary mapping Pygments
116 token types to strings defining the style. See `Pygments' documentation
116 token types to strings defining the style. See `Pygments' documentation
117 <http://pygments.org/docs/styles/#creating-own-styles>`__ for the language used
117 <http://pygments.org/docs/styles/#creating-own-styles>`__ for the language used
118 to define styles.
118 to define styles.
119
119
120 Colors in the pager
120 Colors in the pager
121 -------------------
121 -------------------
122
122
123 On some systems, the default pager has problems with ANSI colour codes.
123 On some systems, the default pager has problems with ANSI colour codes.
124 To configure your default pager to allow these:
124 To configure your default pager to allow these:
125
125
126 1. Set the environment PAGER variable to ``less``.
126 1. Set the environment PAGER variable to ``less``.
127 2. Set the environment LESS variable to ``-r`` (plus any other options
127 2. Set the environment LESS variable to ``-r`` (plus any other options
128 you always want to pass to less by default). This tells less to
128 you always want to pass to less by default). This tells less to
129 properly interpret control sequences, which is how color
129 properly interpret control sequences, which is how color
130 information is given to your terminal.
130 information is given to your terminal.
131
131
132 .. _editors:
132 .. _editors:
133
133
134 Editor configuration
134 Editor configuration
135 ====================
135 ====================
136
136
137 IPython can integrate with text editors in a number of different ways:
137 IPython can integrate with text editors in a number of different ways:
138
138
139 * Editors (such as `(X)Emacs`_, vim_ and TextMate_) can
139 * Editors (such as `(X)Emacs`_, vim_ and TextMate_) can
140 send code to IPython for execution.
140 send code to IPython for execution.
141
141
142 * IPython's ``%edit`` magic command can open an editor of choice to edit
142 * IPython's ``%edit`` magic command can open an editor of choice to edit
143 a code block.
143 a code block.
144
144
145 The %edit command (and its alias %ed) will invoke the editor set in your
145 The %edit command (and its alias %ed) will invoke the editor set in your
146 environment as :envvar:`EDITOR`. If this variable is not set, it will default
146 environment as :envvar:`EDITOR`. If this variable is not set, it will default
147 to vi under Linux/Unix and to notepad under Windows. You may want to set this
147 to vi under Linux/Unix and to notepad under Windows. You may want to set this
148 variable properly and to a lightweight editor which doesn't take too long to
148 variable properly and to a lightweight editor which doesn't take too long to
149 start (that is, something other than a new instance of Emacs). This way you
149 start (that is, something other than a new instance of Emacs). This way you
150 can edit multi-line code quickly and with the power of a real editor right
150 can edit multi-line code quickly and with the power of a real editor right
151 inside IPython.
151 inside IPython.
152
152
153 You can also control the editor by setting :attr:`TerminalInteractiveShell.editor`
153 You can also control the editor by setting :attr:`TerminalInteractiveShell.editor`
154 in :file:`ipython_config.py`.
154 in :file:`ipython_config.py`.
155
155
156 Vim
156 Vim
157 ---
157 ---
158
158
159 Paul Ivanov's `vim-ipython <https://github.com/ivanov/vim-ipython>`_ provides
159 Paul Ivanov's `vim-ipython <https://github.com/ivanov/vim-ipython>`_ provides
160 powerful IPython integration for vim.
160 powerful IPython integration for vim.
161
161
162 .. _emacs:
162 .. _emacs:
163
163
164 (X)Emacs
164 (X)Emacs
165 --------
165 --------
166
166
167 If you are a dedicated Emacs user, and want to use Emacs when IPython's
167 If you are a dedicated Emacs user, and want to use Emacs when IPython's
168 ``%edit`` magic command is called you should set up the Emacs server so that
168 ``%edit`` magic command is called you should set up the Emacs server so that
169 new requests are handled by the original process. This means that almost no
169 new requests are handled by the original process. This means that almost no
170 time is spent in handling the request (assuming an Emacs process is already
170 time is spent in handling the request (assuming an Emacs process is already
171 running). For this to work, you need to set your EDITOR environment variable
171 running). For this to work, you need to set your EDITOR environment variable
172 to 'emacsclient'. The code below, supplied by Francois Pinard, can then be
172 to 'emacsclient'. The code below, supplied by Francois Pinard, can then be
173 used in your :file:`.emacs` file to enable the server:
173 used in your :file:`.emacs` file to enable the server:
174
174
175 .. code-block:: common-lisp
175 .. code-block:: common-lisp
176
176
177 (defvar server-buffer-clients)
177 (defvar server-buffer-clients)
178 (when (and (fboundp 'server-start) (string-equal (getenv "TERM") 'xterm))
178 (when (and (fboundp 'server-start) (string-equal (getenv "TERM") 'xterm))
179 (server-start)
179 (server-start)
180 (defun fp-kill-server-with-buffer-routine ()
180 (defun fp-kill-server-with-buffer-routine ()
181 (and server-buffer-clients (server-done)))
181 (and server-buffer-clients (server-done)))
182 (add-hook 'kill-buffer-hook 'fp-kill-server-with-buffer-routine))
182 (add-hook 'kill-buffer-hook 'fp-kill-server-with-buffer-routine))
183
183
184 Thanks to the work of Alexander Schmolck and Prabhu Ramachandran,
184 Thanks to the work of Alexander Schmolck and Prabhu Ramachandran,
185 currently (X)Emacs and IPython get along very well in other ways.
185 currently (X)Emacs and IPython get along very well in other ways.
186
186
187 With (X)EMacs >= 24, You can enable IPython in python-mode with:
187 With (X)EMacs >= 24, You can enable IPython in python-mode with:
188
188
189 .. code-block:: common-lisp
189 .. code-block:: common-lisp
190
190
191 (require 'python)
191 (require 'python)
192 (setq python-shell-interpreter "ipython")
192 (setq python-shell-interpreter "ipython")
193
193
194 .. _`(X)Emacs`: http://www.gnu.org/software/emacs/
194 .. _`(X)Emacs`: http://www.gnu.org/software/emacs/
195 .. _TextMate: http://macromates.com/
195 .. _TextMate: http://macromates.com/
196 .. _vim: http://www.vim.org/
196 .. _vim: http://www.vim.org/
197
197
198 .. _custom_keyboard_shortcuts:
198 .. _custom_keyboard_shortcuts:
199
199
200 Keyboard Shortcuts
200 Keyboard Shortcuts
201 ==================
201 ==================
202
202
203 .. versionadded:: 8.11
204
205 You can modify, disable or modify keyboard shortcuts for IPython Terminal using
206 :std:configtrait:`TerminalInteractiveShell.shortcuts` traitlet.
207
208 The list of shortcuts is available in the Configuring IPython :ref:`terminal-shortcuts-list` section.
209
210 Advanced configuration
211 ----------------------
212
203 .. versionchanged:: 5.0
213 .. versionchanged:: 5.0
204
214
205 You can customise keyboard shortcuts for terminal IPython. Put code like this in
215 Creating custom commands requires adding custom code to a
206 a :ref:`startup file <startup_files>`::
216 :ref:`startup file <startup_files>`::
207
217
208 from IPython import get_ipython
218 from IPython import get_ipython
209 from prompt_toolkit.enums import DEFAULT_BUFFER
219 from prompt_toolkit.enums import DEFAULT_BUFFER
210 from prompt_toolkit.keys import Keys
220 from prompt_toolkit.keys import Keys
211 from prompt_toolkit.filters import HasFocus, HasSelection, ViInsertMode, EmacsInsertMode
221 from prompt_toolkit.filters import HasFocus, HasSelection, ViInsertMode, EmacsInsertMode
212
222
213 ip = get_ipython()
223 ip = get_ipython()
214 insert_mode = ViInsertMode() | EmacsInsertMode()
224 insert_mode = ViInsertMode() | EmacsInsertMode()
215
225
216 def insert_unexpected(event):
226 def insert_unexpected(event):
217 buf = event.current_buffer
227 buf = event.current_buffer
218 buf.insert_text('The Spanish Inquisition')
228 buf.insert_text('The Spanish Inquisition')
219 # Register the shortcut if IPython is using prompt_toolkit
229 # Register the shortcut if IPython is using prompt_toolkit
220 if getattr(ip, 'pt_app', None):
230 if getattr(ip, 'pt_app', None):
221 registry = ip.pt_app.key_bindings
231 registry = ip.pt_app.key_bindings
222 registry.add_binding(Keys.ControlN,
232 registry.add_binding(Keys.ControlN,
223 filter=(HasFocus(DEFAULT_BUFFER)
233 filter=(HasFocus(DEFAULT_BUFFER)
224 & ~HasSelection()
234 & ~HasSelection()
225 & insert_mode))(insert_unexpected)
235 & insert_mode))(insert_unexpected)
226
236
227
237
228 Here is a second example that bind the key sequence ``j``, ``k`` to switch to
238 Here is a second example that bind the key sequence ``j``, ``k`` to switch to
229 VI input mode to ``Normal`` when in insert mode::
239 VI input mode to ``Normal`` when in insert mode::
230
240
231 from IPython import get_ipython
241 from IPython import get_ipython
232 from prompt_toolkit.enums import DEFAULT_BUFFER
242 from prompt_toolkit.enums import DEFAULT_BUFFER
233 from prompt_toolkit.filters import HasFocus, ViInsertMode
243 from prompt_toolkit.filters import HasFocus, ViInsertMode
234 from prompt_toolkit.key_binding.vi_state import InputMode
244 from prompt_toolkit.key_binding.vi_state import InputMode
235
245
236 ip = get_ipython()
246 ip = get_ipython()
237
247
238 def switch_to_navigation_mode(event):
248 def switch_to_navigation_mode(event):
239 vi_state = event.cli.vi_state
249 vi_state = event.cli.vi_state
240 vi_state.input_mode = InputMode.NAVIGATION
250 vi_state.input_mode = InputMode.NAVIGATION
241
251
242 if getattr(ip, 'pt_app', None):
252 if getattr(ip, 'pt_app', None):
243 registry = ip.pt_app.key_bindings
253 registry = ip.pt_app.key_bindings
244 registry.add_binding(u'j',u'k',
254 registry.add_binding(u'j',u'k',
245 filter=(HasFocus(DEFAULT_BUFFER)
255 filter=(HasFocus(DEFAULT_BUFFER)
246 & ViInsertMode()))(switch_to_navigation_mode)
256 & ViInsertMode()))(switch_to_navigation_mode)
247
257
248 For more information on filters and what you can do with the ``event`` object,
258 For more information on filters and what you can do with the ``event`` object,
249 `see the prompt_toolkit docs
259 `see the prompt_toolkit docs
250 <https://python-prompt-toolkit.readthedocs.io/en/latest/pages/asking_for_input.html#adding-custom-key-bindings>`__.
260 <https://python-prompt-toolkit.readthedocs.io/en/latest/pages/asking_for_input.html#adding-custom-key-bindings>`__.
251
261
252
262
253 Enter to execute
263 Enter to execute
254 ----------------
264 ----------------
255
265
256 In the Terminal IPython shell – which by default uses the ``prompt_toolkit``
266 In the Terminal IPython shell – which by default uses the ``prompt_toolkit``
257 interface, the semantic meaning of pressing the :kbd:`Enter` key can be
267 interface, the semantic meaning of pressing the :kbd:`Enter` key can be
258 ambiguous. In some case :kbd:`Enter` should execute code, and in others it
268 ambiguous. In some case :kbd:`Enter` should execute code, and in others it
259 should add a new line. IPython uses heuristics to decide whether to execute or
269 should add a new line. IPython uses heuristics to decide whether to execute or
260 insert a new line at cursor position. For example, if we detect that the current
270 insert a new line at cursor position. For example, if we detect that the current
261 code is not valid Python, then the user is likely editing code and the right
271 code is not valid Python, then the user is likely editing code and the right
262 behavior is to likely to insert a new line. If the current code is a simple
272 behavior is to likely to insert a new line. If the current code is a simple
263 statement like `ord('*')`, then the right behavior is likely to execute. Though
273 statement like `ord('*')`, then the right behavior is likely to execute. Though
264 the exact desired semantics often varies from users to users.
274 the exact desired semantics often varies from users to users.
265
275
266 As the exact behavior of :kbd:`Enter` is ambiguous, it has been special cased
276 As the exact behavior of :kbd:`Enter` is ambiguous, it has been special cased
267 to allow users to completely configure the behavior they like. Hence you can
277 to allow users to completely configure the behavior they like. Hence you can
268 have enter always execute code. If you prefer fancier behavior, you need to get
278 have enter always execute code. If you prefer fancier behavior, you need to get
269 your hands dirty and read the ``prompt_toolkit`` and IPython documentation
279 your hands dirty and read the ``prompt_toolkit`` and IPython documentation
270 though. See :ghpull:`10500`, set the
280 though. See :ghpull:`10500`, set the
271 ``c.TerminalInteractiveShell.handle_return`` option and get inspiration from the
281 ``c.TerminalInteractiveShell.handle_return`` option and get inspiration from the
272 following example that only auto-executes the input if it begins with a bang or
282 following example that only auto-executes the input if it begins with a bang or
273 a modulo character (``!`` or ``%``). To use the following code, add it to your
283 a modulo character (``!`` or ``%``). To use the following code, add it to your
274 IPython configuration::
284 IPython configuration::
275
285
276 def custom_return(shell):
286 def custom_return(shell):
277
287
278 """This function is required by the API. It takes a reference to
288 """This function is required by the API. It takes a reference to
279 the shell, which is the same thing `get_ipython()` evaluates to.
289 the shell, which is the same thing `get_ipython()` evaluates to.
280 This function must return a function that handles each keypress
290 This function must return a function that handles each keypress
281 event. That function, named `handle` here, references `shell`
291 event. That function, named `handle` here, references `shell`
282 by closure."""
292 by closure."""
283
293
284 def handle(event):
294 def handle(event):
285
295
286 """This function is called each time `Enter` is pressed,
296 """This function is called each time `Enter` is pressed,
287 and takes a reference to a Prompt Toolkit event object.
297 and takes a reference to a Prompt Toolkit event object.
288 If the current input starts with a bang or modulo, then
298 If the current input starts with a bang or modulo, then
289 the input is executed, otherwise a newline is entered,
299 the input is executed, otherwise a newline is entered,
290 followed by any spaces needed to auto-indent."""
300 followed by any spaces needed to auto-indent."""
291
301
292 # set up a few handy references to nested items...
302 # set up a few handy references to nested items...
293
303
294 buffer = event.current_buffer
304 buffer = event.current_buffer
295 document = buffer.document
305 document = buffer.document
296 text = document.text
306 text = document.text
297
307
298 if text.startswith('!') or text.startswith('%'): # execute the input...
308 if text.startswith('!') or text.startswith('%'): # execute the input...
299
309
300 buffer.accept_action.validate_and_handle(event.cli, buffer)
310 buffer.accept_action.validate_and_handle(event.cli, buffer)
301
311
302 else: # insert a newline with auto-indentation...
312 else: # insert a newline with auto-indentation...
303
313
304 if document.line_count > 1: text = text[:document.cursor_position]
314 if document.line_count > 1: text = text[:document.cursor_position]
305 indent = shell.check_complete(text)[1]
315 indent = shell.check_complete(text)[1]
306 buffer.insert_text('\n' + indent)
316 buffer.insert_text('\n' + indent)
307
317
308 # if you just wanted a plain newline without any indentation, you
318 # if you just wanted a plain newline without any indentation, you
309 # could use `buffer.insert_text('\n')` instead of the lines above
319 # could use `buffer.insert_text('\n')` instead of the lines above
310
320
311 return handle
321 return handle
312
322
313 c.TerminalInteractiveShell.handle_return = custom_return
323 c.TerminalInteractiveShell.handle_return = custom_return
@@ -1,26 +1,31 b''
1 .. _terminal-shortcuts-list:
2
1 =================
3 =================
2 IPython shortcuts
4 IPython shortcuts
3 =================
5 =================
4
6
5 Available shortcuts in an IPython terminal.
7 Shortcuts available in an IPython terminal.
6
8
7 .. note::
9 .. note::
8
10
9 This list is automatically generated. Key bindings defined in ``prompt_toolkit`` may differ
11 This list is automatically generated. Key bindings defined in ``prompt_toolkit`` may differ
10 between installations depending on the ``prompt_toolkit`` version.
12 between installations depending on the ``prompt_toolkit`` version.
11
13
12
14
13 * Comma-separated keys, e.g. :kbd:`Esc`, :kbd:`f`, indicate a sequence which can be activated by pressing the listed keys in succession.
15 * Comma-separated keys, e.g. :kbd:`Esc`, :kbd:`f`, indicate a sequence which can be activated by pressing the listed keys in succession.
14 * Plus-separated keys, e.g. :kbd:`Esc` + :kbd:`f` indicate a combination which requires pressing all keys simultaneously.
16 * Plus-separated keys, e.g. :kbd:`Esc` + :kbd:`f` indicate a combination which requires pressing all keys simultaneously.
15 * Hover over the ⓘ icon in the filter column to see when the shortcut is active.g
17 * Hover over the ⓘ icon in the filter column to see when the shortcut is active.
18
19 You can use :std:configtrait:`TerminalInteractiveShell.shortcuts` configuration
20 to modify, disable or add shortcuts.
16
21
17 .. role:: raw-html(raw)
22 .. role:: raw-html(raw)
18 :format: html
23 :format: html
19
24
20
25
21 .. csv-table::
26 .. csv-table::
22 :header: Shortcut,Description and identifier,Filter
27 :header: Shortcut,Description and identifier,Filter
23 :delim: tab
28 :delim: tab
24 :class: shortcuts
29 :class: shortcuts
25 :file: table.tsv
30 :file: table.tsv
26 :widths: 20 75 5
31 :widths: 20 75 5
General Comments 0
You need to be logged in to leave comments. Login now