##// 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"]
@@ -16,6 +16,7 b' from traitlets import ('
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,
@@ -29,7 +30,7 b' from traitlets import ('
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
@@ -49,7 +50,14 b' 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,
@@ -415,6 +423,145 b' class TerminalInteractiveShell(InteractiveShell):'
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)
@@ -477,7 +624,7 b' class TerminalInteractiveShell(InteractiveShell):'
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)
This diff has been collapsed as it changes many lines, (736 lines changed) Show them Hide them
@@ -7,414 +7,263 b' Module to define and register Terminal IPython shortcuts with'
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 import os
9 import os
10 import re
11 import signal
10 import signal
12 import sys
11 import sys
13 import warnings
12 import warnings
14 from typing import Callable, Dict, Union
13 from dataclasses import dataclass
14 from typing import Callable, Any, Optional, List
15
15
16 from prompt_toolkit.application.current import get_app
16 from prompt_toolkit.application.current import get_app
17 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
18 from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions
19 from prompt_toolkit.filters import has_focus as has_focus_impl
20 from prompt_toolkit.filters import (
21 has_selection,
22 has_suggestion,
23 vi_insert_mode,
24 vi_mode,
25 )
26 from prompt_toolkit.key_binding import KeyBindings
17 from prompt_toolkit.key_binding import KeyBindings
18 from prompt_toolkit.key_binding.key_processor import KeyPressEvent
27 from prompt_toolkit.key_binding.bindings import named_commands as nc
19 from prompt_toolkit.key_binding.bindings import named_commands as nc
28 from prompt_toolkit.key_binding.bindings.completion import (
20 from prompt_toolkit.key_binding.bindings.completion import (
29 display_completions_like_readline,
21 display_completions_like_readline,
30 )
22 )
31 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
23 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
32 from prompt_toolkit.layout.layout import FocusableElement
24 from prompt_toolkit.filters import Condition
33
25
26 from IPython.core.getipython import get_ipython
34 from IPython.terminal.shortcuts import auto_match as match
27 from IPython.terminal.shortcuts import auto_match as match
35 from IPython.terminal.shortcuts import auto_suggest
28 from IPython.terminal.shortcuts import auto_suggest
29 from IPython.terminal.shortcuts.filters import filter_from_string
36 from IPython.utils.decorators import undoc
30 from IPython.utils.decorators import undoc
37
31
38 __all__ = ["create_ipython_shortcuts"]
32 __all__ = ["create_ipython_shortcuts"]
39
33
40
34
41 @undoc
35 @dataclass
42 @Condition
36 class BaseBinding:
43 def cursor_in_leading_ws():
37 command: Callable[[KeyPressEvent], Any]
44 before = get_app().current_buffer.document.current_line_before_cursor
38 keys: List[str]
45 return (not before) or before.isspace()
46
47
48 def has_focus(value: FocusableElement):
49 """Wrapper around has_focus adding a nice `__name__` to tester function"""
50 tester = has_focus_impl(value).func
51 tester.__name__ = f"is_focused({value})"
52 return Condition(tester)
53
54
55 @undoc
56 @Condition
57 def has_line_below() -> bool:
58 document = get_app().current_buffer.document
59 return document.cursor_position_row < len(document.lines) - 1
60
61
62 @undoc
63 @Condition
64 def has_line_above() -> bool:
65 document = get_app().current_buffer.document
66 return document.cursor_position_row != 0
67
68
69 def create_ipython_shortcuts(shell, for_all_platforms: bool = False) -> KeyBindings:
70 """Set up the prompt_toolkit keyboard shortcuts for IPython.
71
72 Parameters
73 ----------
74 shell: InteractiveShell
75 The current IPython shell Instance
76 for_all_platforms: bool (default false)
77 This parameter is mostly used in generating the documentation
78 to create the shortcut binding for all the platforms, and export
79 them.
80
81 Returns
82 -------
83 KeyBindings
84 the keybinding instance for prompt toolkit.
85
39
86 """
87 # Warning: if possible, do NOT define handler functions in the locals
88 # scope of this function, instead define functions in the global
89 # scope, or a separate module, and include a user-friendly docstring
90 # describing the action.
91
40
92 kb = KeyBindings()
41 @dataclass
93 insert_mode = vi_insert_mode | emacs_insert_mode
42 class RuntimeBinding(BaseBinding):
43 filter: Condition
94
44
95 if getattr(shell, "handle_return", None):
96 return_handler = shell.handle_return(shell)
97 else:
98 return_handler = newline_or_execute_outer(shell)
99
45
100 kb.add("enter", filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode))(
46 @dataclass
101 return_handler
47 class Binding(BaseBinding):
102 )
48 # while filter could be created by referencing variables directly (rather
103
49 # than created from strings), by using strings we ensure that users will
104 @Condition
50 # be able to create filters in configuration (e.g. JSON) files too, which
105 def ebivim():
51 # also benefits the documentation by enforcing human-readable filter names.
106 return shell.emacs_bindings_in_vi_insert_mode
52 condition: Optional[str] = None
107
108 @kb.add(
109 "escape",
110 "enter",
111 filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode & ebivim),
112 )
113 def reformat_and_execute(event):
114 """Reformat code and execute it"""
115 reformat_text_before_cursor(
116 event.current_buffer, event.current_buffer.document, shell
117 )
118 event.current_buffer.validate_and_handle()
119
120 kb.add("c-\\")(quit)
121
122 kb.add("c-p", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))(
123 previous_history_or_previous_completion
124 )
125
126 kb.add("c-n", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))(
127 next_history_or_next_completion
128 )
129
130 kb.add("c-g", filter=(has_focus(DEFAULT_BUFFER) & has_completions))(
131 dismiss_completion
132 )
133
134 kb.add("c-c", filter=has_focus(DEFAULT_BUFFER))(reset_buffer)
135
136 kb.add("c-c", filter=has_focus(SEARCH_BUFFER))(reset_search_buffer)
137
138 supports_suspend = Condition(lambda: hasattr(signal, "SIGTSTP"))
139 kb.add("c-z", filter=supports_suspend)(suspend_to_bg)
140
141 # Ctrl+I == Tab
142 kb.add(
143 "tab",
144 filter=(
145 has_focus(DEFAULT_BUFFER)
146 & ~has_selection
147 & insert_mode
148 & cursor_in_leading_ws
149 ),
150 )(indent_buffer)
151 kb.add("c-o", filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode))(
152 newline_autoindent_outer(shell.input_transformer_manager)
153 )
154
155 kb.add("f2", filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor)
156
157 @Condition
158 def auto_match():
159 return shell.auto_match
160
161 def all_quotes_paired(quote, buf):
162 paired = True
163 i = 0
164 while i < len(buf):
165 c = buf[i]
166 if c == quote:
167 paired = not paired
168 elif c == "\\":
169 i += 1
170 i += 1
171 return paired
172
173 focused_insert = (vi_insert_mode | emacs_insert_mode) & has_focus(DEFAULT_BUFFER)
174 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
175 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
176
177 def preceding_text(pattern: Union[str, Callable]):
178 if pattern in _preceding_text_cache:
179 return _preceding_text_cache[pattern]
180
181 if callable(pattern):
182
183 def _preceding_text():
184 app = get_app()
185 before_cursor = app.current_buffer.document.current_line_before_cursor
186 # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
187 return bool(pattern(before_cursor)) # type: ignore[operator]
188
53
54 def __post_init__(self):
55 if self.condition:
56 self.filter = filter_from_string(self.condition)
189 else:
57 else:
190 m = re.compile(pattern)
58 self.filter = None
191
192 def _preceding_text():
193 app = get_app()
194 before_cursor = app.current_buffer.document.current_line_before_cursor
195 return bool(m.match(before_cursor))
196
197 _preceding_text.__name__ = f"preceding_text({pattern!r})"
198
199 condition = Condition(_preceding_text)
200 _preceding_text_cache[pattern] = condition
201 return condition
202
203 def following_text(pattern):
204 try:
205 return _following_text_cache[pattern]
206 except KeyError:
207 pass
208 m = re.compile(pattern)
209
210 def _following_text():
211 app = get_app()
212 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
213
214 _following_text.__name__ = f"following_text({pattern!r})"
215
59
216 condition = Condition(_following_text)
217 _following_text_cache[pattern] = condition
218 return condition
219
60
220 @Condition
61 def create_identifier(handler: Callable):
221 def not_inside_unclosed_string():
62 parts = handler.__module__.split(".")
222 app = get_app()
63 name = handler.__name__
223 s = app.current_buffer.document.text_before_cursor
64 package = parts[0]
224 # remove escaped quotes
65 if len(parts) > 1:
225 s = s.replace('\\"', "").replace("\\'", "")
66 final_module = parts[-1]
226 # remove triple-quoted string literals
67 return f"{package}:{final_module}.{name}"
227 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
68 else:
228 # remove single-quoted string literals
69 return f"{package}:{name}"
229 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
230 return not ('"' in s or "'" in s)
231
232 # auto match
233 for key, cmd in match.auto_match_parens.items():
234 kb.add(key, filter=focused_insert & auto_match & following_text(r"[,)}\]]|$"))(
235 cmd
236 )
237
70
238 # raw string
239 for key, cmd in match.auto_match_parens_raw_string.items():
240 kb.add(
241 key,
242 filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$"),
243 )(cmd)
244
245 kb.add(
246 '"',
247 filter=focused_insert
248 & auto_match
249 & not_inside_unclosed_string
250 & preceding_text(lambda line: all_quotes_paired('"', line))
251 & following_text(r"[,)}\]]|$"),
252 )(match.double_quote)
253
254 kb.add(
255 "'",
256 filter=focused_insert
257 & auto_match
258 & not_inside_unclosed_string
259 & preceding_text(lambda line: all_quotes_paired("'", line))
260 & following_text(r"[,)}\]]|$"),
261 )(match.single_quote)
262
263 kb.add(
264 '"',
265 filter=focused_insert
266 & auto_match
267 & not_inside_unclosed_string
268 & preceding_text(r'^.*""$'),
269 )(match.docstring_double_quotes)
270
271 kb.add(
272 "'",
273 filter=focused_insert
274 & auto_match
275 & not_inside_unclosed_string
276 & preceding_text(r"^.*''$"),
277 )(match.docstring_single_quotes)
278
279 # just move cursor
280 kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)"))(
281 match.skip_over
282 )
283 kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]"))(
284 match.skip_over
285 )
286 kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}"))(
287 match.skip_over
288 )
289 kb.add('"', filter=focused_insert & auto_match & following_text('^"'))(
290 match.skip_over
291 )
292 kb.add("'", filter=focused_insert & auto_match & following_text("^'"))(
293 match.skip_over
294 )
295
71
296 kb.add(
72 AUTO_MATCH_BINDINGS = [
297 "backspace",
73 *[
298 filter=focused_insert
74 Binding(
299 & preceding_text(r".*\($")
75 cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
300 & auto_match
301 & following_text(r"^\)"),
302 )(match.delete_pair)
303 kb.add(
304 "backspace",
305 filter=focused_insert
306 & preceding_text(r".*\[$")
307 & auto_match
308 & following_text(r"^\]"),
309 )(match.delete_pair)
310 kb.add(
311 "backspace",
312 filter=focused_insert
313 & preceding_text(r".*\{$")
314 & auto_match
315 & following_text(r"^\}"),
316 )(match.delete_pair)
317 kb.add(
318 "backspace",
319 filter=focused_insert
320 & preceding_text('.*"$')
321 & auto_match
322 & following_text('^"'),
323 )(match.delete_pair)
324 kb.add(
325 "backspace",
326 filter=focused_insert
327 & preceding_text(r".*'$")
328 & auto_match
329 & following_text(r"^'"),
330 )(match.delete_pair)
331
332 if shell.display_completions == "readlinelike":
333 kb.add(
334 "c-i",
335 filter=(
336 has_focus(DEFAULT_BUFFER)
337 & ~has_selection
338 & insert_mode
339 & ~cursor_in_leading_ws
340 ),
341 )(display_completions_like_readline)
342
343 if sys.platform == "win32" or for_all_platforms:
344 kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste)
345
346 focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode
347
348 # autosuggestions
349 @Condition
350 def navigable_suggestions():
351 return isinstance(
352 shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory
353 )
76 )
354
77 for key, cmd in match.auto_match_parens.items()
355 kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))(
78 ],
356 auto_suggest.accept_in_vi_insert_mode
79 *[
357 )
80 # raw string
358 kb.add("c-e", filter=focused_insert_vi & ebivim)(
81 Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
359 auto_suggest.accept_in_vi_insert_mode
82 for key, cmd in match.auto_match_parens_raw_string.items()
360 )
83 ],
361 kb.add("c-f", filter=focused_insert_vi)(auto_suggest.accept)
84 Binding(
362 kb.add("escape", "f", filter=focused_insert_vi & ebivim)(auto_suggest.accept_word)
85 match.double_quote,
363 kb.add("c-right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
86 ['"'],
364 auto_suggest.accept_token
87 "focused_insert"
365 )
88 " & auto_match"
366 kb.add(
89 " & not_inside_unclosed_string"
367 "escape", filter=has_suggestion & has_focus(DEFAULT_BUFFER) & emacs_insert_mode
90 " & preceded_by_paired_double_quotes"
368 )(auto_suggest.discard)
91 " & followed_by_closing_paren_or_end",
369 kb.add(
92 ),
370 "up",
93 Binding(
371 filter=navigable_suggestions
94 match.single_quote,
372 & ~has_line_above
95 ["'"],
373 & has_suggestion
96 "focused_insert"
374 & has_focus(DEFAULT_BUFFER),
97 " & auto_match"
375 )(auto_suggest.swap_autosuggestion_up(shell.auto_suggest))
98 " & not_inside_unclosed_string"
376 kb.add(
99 " & preceded_by_paired_single_quotes"
377 "down",
100 " & followed_by_closing_paren_or_end",
378 filter=navigable_suggestions
101 ),
379 & ~has_line_below
102 Binding(
380 & has_suggestion
103 match.docstring_double_quotes,
381 & has_focus(DEFAULT_BUFFER),
104 ['"'],
382 )(auto_suggest.swap_autosuggestion_down(shell.auto_suggest))
105 "focused_insert"
383 kb.add(
106 " & auto_match"
384 "up", filter=has_line_above & navigable_suggestions & has_focus(DEFAULT_BUFFER)
107 " & not_inside_unclosed_string"
385 )(auto_suggest.up_and_update_hint)
108 " & preceded_by_two_double_quotes",
386 kb.add(
109 ),
387 "down",
110 Binding(
388 filter=has_line_below & navigable_suggestions & has_focus(DEFAULT_BUFFER),
111 match.docstring_single_quotes,
389 )(auto_suggest.down_and_update_hint)
112 ["'"],
390 kb.add("right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
113 "focused_insert"
391 auto_suggest.accept_character
114 " & auto_match"
392 )
115 " & not_inside_unclosed_string"
393 kb.add("c-left", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
116 " & preceded_by_two_single_quotes",
394 auto_suggest.accept_and_move_cursor_left
117 ),
395 )
118 Binding(
396 kb.add("c-down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
119 match.skip_over,
397 auto_suggest.accept_and_keep_cursor
120 [")"],
398 )
121 "focused_insert & auto_match & followed_by_closing_round_paren",
399 kb.add("backspace", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
122 ),
400 auto_suggest.backspace_and_resume_hint
123 Binding(
401 )
124 match.skip_over,
402
125 ["]"],
403 # Simple Control keybindings
126 "focused_insert & auto_match & followed_by_closing_bracket",
404 key_cmd_dict = {
127 ),
128 Binding(
129 match.skip_over,
130 ["}"],
131 "focused_insert & auto_match & followed_by_closing_brace",
132 ),
133 Binding(
134 match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
135 ),
136 Binding(
137 match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
138 ),
139 Binding(
140 match.delete_pair,
141 ["backspace"],
142 "focused_insert"
143 " & preceded_by_opening_round_paren"
144 " & auto_match"
145 " & followed_by_closing_round_paren",
146 ),
147 Binding(
148 match.delete_pair,
149 ["backspace"],
150 "focused_insert"
151 " & preceded_by_opening_bracket"
152 " & auto_match"
153 " & followed_by_closing_bracket",
154 ),
155 Binding(
156 match.delete_pair,
157 ["backspace"],
158 "focused_insert"
159 " & preceded_by_opening_brace"
160 " & auto_match"
161 " & followed_by_closing_brace",
162 ),
163 Binding(
164 match.delete_pair,
165 ["backspace"],
166 "focused_insert"
167 " & preceded_by_double_quote"
168 " & auto_match"
169 " & followed_by_double_quote",
170 ),
171 Binding(
172 match.delete_pair,
173 ["backspace"],
174 "focused_insert"
175 " & preceded_by_single_quote"
176 " & auto_match"
177 " & followed_by_single_quote",
178 ),
179 ]
180
181 AUTO_SUGGEST_BINDINGS = [
182 Binding(
183 auto_suggest.accept_in_vi_insert_mode,
184 ["end"],
185 "default_buffer_focused & (ebivim | ~vi_insert_mode)",
186 ),
187 Binding(
188 auto_suggest.accept_in_vi_insert_mode, ["c-e"], "focused_insert_vi & ebivim"
189 ),
190 Binding(auto_suggest.accept, ["c-f"], "focused_insert_vi"),
191 Binding(auto_suggest.accept_word, ["escape", "f"], "focused_insert_vi & ebivim"),
192 Binding(
193 auto_suggest.accept_token,
194 ["c-right"],
195 "has_suggestion & default_buffer_focused",
196 ),
197 Binding(
198 auto_suggest.discard,
199 ["escape"],
200 "has_suggestion & default_buffer_focused & emacs_insert_mode",
201 ),
202 Binding(
203 auto_suggest.swap_autosuggestion_up,
204 ["up"],
205 "navigable_suggestions"
206 " & ~has_line_above"
207 " & has_suggestion"
208 " & default_buffer_focused",
209 ),
210 Binding(
211 auto_suggest.swap_autosuggestion_down,
212 ["down"],
213 "navigable_suggestions"
214 " & ~has_line_below"
215 " & has_suggestion"
216 " & default_buffer_focused",
217 ),
218 Binding(
219 auto_suggest.up_and_update_hint,
220 ["up"],
221 "has_line_above & navigable_suggestions & default_buffer_focused",
222 ),
223 Binding(
224 auto_suggest.down_and_update_hint,
225 ["down"],
226 "has_line_below & navigable_suggestions & default_buffer_focused",
227 ),
228 Binding(
229 auto_suggest.accept_character,
230 ["right"],
231 "has_suggestion & default_buffer_focused",
232 ),
233 Binding(
234 auto_suggest.accept_and_move_cursor_left,
235 ["c-left"],
236 "has_suggestion & default_buffer_focused",
237 ),
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 ]
249
250
251 SIMPLE_CONTROL_BINDINGS = [
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,
@@ -427,10 +276,48 b' def create_ipython_shortcuts(shell, for_all_platforms: bool = False) -> KeyBindi'
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.
431
292
432 for keys, cmd in keys_cmd_dict.items():
293 Parameters
433 kb.add(*keys, filter=focused_insert_vi & ebivim)(cmd)
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.
304
305 """
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()
@@ -451,9 +338,19 b' def create_ipython_shortcuts(shell, for_all_platforms: bool = False) -> KeyBindi'
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:
@@ -463,6 +360,14 b' def reformat_text_before_cursor(buffer, document, shell):'
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."""
@@ -512,8 +417,6 b' def newline_or_execute_outer(shell):'
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
@@ -610,30 +513,24 b' def newline_with_copy_margin(event):'
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 """
524 shell = get_ipython()
525 inputsplitter = shell.input_transformer_manager
526 b = event.current_buffer
527 d = b.document
622
528
623 def newline_autoindent(event):
529 if b.complete_state:
624 """Insert a newline after the cursor indented appropriately."""
530 b.cancel_completion()
625 b = event.current_buffer
531 text = d.text[: d.cursor_position] + "\n"
626 d = b.document
532 _, indent = inputsplitter.check_complete(text)
627
533 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
628 if b.complete_state:
629 b.cancel_completion()
630 text = d.text[: d.cursor_position] + "\n"
631 _, indent = inputsplitter.check_complete(text)
632 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
633
634 newline_autoindent.__qualname__ = "newline_autoindent"
635
636 return newline_autoindent
637
534
638
535
639 def open_input_in_editor(event):
536 def open_input_in_editor(event):
@@ -666,5 +563,58 b' 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,7 +1,7 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
@@ -84,9 +84,9 b' def raw_string_braces(event: KeyPressEvent):'
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
@@ -1,7 +1,7 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
@@ -16,6 +16,7 b' from prompt_toolkit.layout.processors import ('
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
@@ -346,33 +347,29 b' def _swap_autosuggestion('
346 buffer.suggestion = new_suggestion
347 buffer.suggestion = new_suggestion
347
348
348
349
349 def swap_autosuggestion_up(provider: Provider):
350 def swap_autosuggestion_up(event: KeyPressEvent):
350 def swap_autosuggestion_up(event: KeyPressEvent):
351 """Get next autosuggestion from history."""
351 """Get next autosuggestion from history."""
352 shell = get_ipython()
352 if not isinstance(provider, NavigableAutoSuggestFromHistory):
353 provider = shell.auto_suggest
353 return
354
354
355 return _swap_autosuggestion(
355 if not isinstance(provider, NavigableAutoSuggestFromHistory):
356 buffer=event.current_buffer, provider=provider, direction_method=provider.up
356 return
357 )
358
357
359 swap_autosuggestion_up.__name__ = "swap_autosuggestion_up"
358 return _swap_autosuggestion(
360 return swap_autosuggestion_up
359 buffer=event.current_buffer, provider=provider, direction_method=provider.up
360 )
361
361
362
362
363 def swap_autosuggestion_down(
363 def swap_autosuggestion_down(event: KeyPressEvent):
364 provider: Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
364 """Get previous autosuggestion from history."""
365 ):
365 shell = get_ipython()
366 def swap_autosuggestion_down(event: KeyPressEvent):
366 provider = shell.auto_suggest
367 """Get previous autosuggestion from history."""
368 if not isinstance(provider, NavigableAutoSuggestFromHistory):
369 return
370
367
371 return _swap_autosuggestion(
368 if not isinstance(provider, NavigableAutoSuggestFromHistory):
372 buffer=event.current_buffer,
369 return
373 provider=provider,
374 direction_method=provider.down,
375 )
376
370
377 swap_autosuggestion_down.__name__ = "swap_autosuggestion_down"
371 return _swap_autosuggestion(
378 return swap_autosuggestion_down
372 buffer=event.current_buffer,
373 provider=provider,
374 direction_method=provider.down,
375 )
@@ -9,9 +9,8 b' 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
@@ -11,6 +11,8 b' from IPython.terminal.shortcuts.auto_suggest import ('
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
@@ -192,18 +194,20 b' def test_autosuggest_token_empty():'
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
@@ -211,8 +215,8 b' async def test_navigable_provider():'
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
@@ -254,14 +258,16 b' 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
@@ -316,3 +322,101 b' def test_navigable_provider_connection():'
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,7 +1,7 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
@@ -10,7 +10,8 b' 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
@@ -44,11 +45,16 b' class _Invert(Filter):'
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,
@@ -58,6 +64,8 b' def format_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)
@@ -66,10 +74,12 b' def format_filter('
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)
@@ -104,17 +114,6 b' class _DummyTerminal:'
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] = []
@@ -178,11 +177,12 b' def format_prompt_keys(keys: str, add_alternatives=True) -> str:'
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
General Comments 0
You need to be logged in to leave comments. Login now