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