##// 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 16 Unicode,
17 17 Dict,
18 18 Integer,
19 List,
19 20 observe,
20 21 Instance,
21 22 Type,
@@ -29,7 +30,7 b' from traitlets import ('
29 30
30 31 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
31 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 34 from prompt_toolkit.formatted_text import PygmentsTokens
34 35 from prompt_toolkit.history import History
35 36 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
@@ -49,7 +50,14 b' from .magics import TerminalMagics'
49 50 from .pt_inputhooks import get_inputhook_name_and_func
50 51 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
51 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 61 from .shortcuts.auto_suggest import (
54 62 NavigableAutoSuggestFromHistory,
55 63 AppendAutoSuggestionInAnyLine,
@@ -415,6 +423,145 b' class TerminalInteractiveShell(InteractiveShell):'
415 423 provider = change.new
416 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 565 prompt_includes_vi_mode = Bool(True,
419 566 help="Display the current vi mode (when using vi editing mode)."
420 567 ).tag(config=True)
@@ -477,7 +624,7 b' class TerminalInteractiveShell(InteractiveShell):'
477 624 enable_open_in_editor=self.extra_open_editor_shortcuts,
478 625 color_depth=self.color_depth,
479 626 tempfile_suffix=".py",
480 **self._extra_prompt_options()
627 **self._extra_prompt_options(),
481 628 )
482 629 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
483 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 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 import os
10 import re
11 10 import signal
12 11 import sys
13 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 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 17 from prompt_toolkit.key_binding import KeyBindings
18 from prompt_toolkit.key_binding.key_processor import KeyPressEvent
27 19 from prompt_toolkit.key_binding.bindings import named_commands as nc
28 20 from prompt_toolkit.key_binding.bindings.completion import (
29 21 display_completions_like_readline,
30 22 )
31 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 27 from IPython.terminal.shortcuts import auto_match as match
35 28 from IPython.terminal.shortcuts import auto_suggest
29 from IPython.terminal.shortcuts.filters import filter_from_string
36 30 from IPython.utils.decorators import undoc
37 31
38 32 __all__ = ["create_ipython_shortcuts"]
39 33
40 34
41 @undoc
42 @Condition
43 def cursor_in_leading_ws():
44 before = get_app().current_buffer.document.current_line_before_cursor
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.
35 @dataclass
36 class BaseBinding:
37 command: Callable[[KeyPressEvent], Any]
38 keys: List[str]
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()
93 insert_mode = vi_insert_mode | emacs_insert_mode
41 @dataclass
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))(
101 return_handler
102 )
103
104 @Condition
105 def ebivim():
106 return shell.emacs_bindings_in_vi_insert_mode
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]
46 @dataclass
47 class Binding(BaseBinding):
48 # while filter could be created by referencing variables directly (rather
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
188 53
54 def __post_init__(self):
55 if self.condition:
56 self.filter = filter_from_string(self.condition)
189 57 else:
190 m = re.compile(pattern)
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})"
58 self.filter = None
215 59
216 condition = Condition(_following_text)
217 _following_text_cache[pattern] = condition
218 return condition
219 60
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 )
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}"
68 else:
69 return f"{package}:{name}"
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(
297 "backspace",
298 filter=focused_insert
299 & preceding_text(r".*\($")
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
72 AUTO_MATCH_BINDINGS = [
73 *[
74 Binding(
75 cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
353 76 )
354
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
403 # Simple Control keybindings
404 key_cmd_dict = {
77 for key, cmd in match.auto_match_parens.items()
78 ],
79 *[
80 # raw string
81 Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
82 for key, cmd in match.auto_match_parens_raw_string.items()
83 ],
84 Binding(
85 match.double_quote,
86 ['"'],
87 "focused_insert"
88 " & auto_match"
89 " & not_inside_unclosed_string"
90 " & preceded_by_paired_double_quotes"
91 " & followed_by_closing_paren_or_end",
92 ),
93 Binding(
94 match.single_quote,
95 ["'"],
96 "focused_insert"
97 " & auto_match"
98 " & not_inside_unclosed_string"
99 " & preceded_by_paired_single_quotes"
100 " & followed_by_closing_paren_or_end",
101 ),
102 Binding(
103 match.docstring_double_quotes,
104 ['"'],
105 "focused_insert"
106 " & auto_match"
107 " & not_inside_unclosed_string"
108 " & preceded_by_two_double_quotes",
109 ),
110 Binding(
111 match.docstring_single_quotes,
112 ["'"],
113 "focused_insert"
114 " & auto_match"
115 " & not_inside_unclosed_string"
116 " & preceded_by_two_single_quotes",
117 ),
118 Binding(
119 match.skip_over,
120 [")"],
121 "focused_insert & auto_match & followed_by_closing_round_paren",
122 ),
123 Binding(
124 match.skip_over,
125 ["]"],
126 "focused_insert & auto_match & followed_by_closing_bracket",
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 254 "c-a": nc.beginning_of_line,
406 255 "c-b": nc.backward_char,
407 256 "c-k": nc.kill_line,
408 257 "c-w": nc.backward_kill_word,
409 258 "c-y": nc.yank,
410 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
417 keys_cmd_dict = {
264 ALT_AND_COMOBO_CONTROL_BINDINGS = [
265 Binding(cmd, list(keys), "focused_insert_vi & ebivim")
266 for keys, cmd in {
418 267 # Control Combos
419 268 ("c-x", "c-e"): nc.edit_and_execute,
420 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 276 ("escape", "u"): nc.uppercase_word,
428 277 ("escape", "y"): nc.yank_pop,
429 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():
433 kb.add(*keys, filter=focused_insert_vi & ebivim)(cmd)
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.
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 322 def get_input_mode(self):
436 323 app = get_app()
@@ -451,9 +338,19 b' def create_ipython_shortcuts(shell, for_all_platforms: bool = False) -> KeyBindi'
451 338 if shell.editing_mode == "vi" and shell.modal_cursor:
452 339 ViState._input_mode = InputMode.INSERT # type: ignore
453 340 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
341
454 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 354 def reformat_text_before_cursor(buffer, document, shell):
458 355 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
459 356 try:
@@ -463,6 +360,14 b' def reformat_text_before_cursor(buffer, document, shell):'
463 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 371 def newline_or_execute_outer(shell):
467 372 def newline_or_execute(event):
468 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 417 else:
513 418 b.insert_text("\n")
514 419
515 newline_or_execute.__qualname__ = "newline_or_execute"
516
517 420 return newline_or_execute
518 421
519 422
@@ -610,30 +513,24 b' def newline_with_copy_margin(event):'
610 513 b.cursor_right(count=pos_diff)
611 514
612 515
613 def newline_autoindent_outer(inputsplitter) -> Callable[..., None]:
614 """
615 Return a function suitable for inserting a indented newline after the cursor.
516 def newline_autoindent(event):
517 """Insert a newline after the cursor indented appropriately.
616 518
617 519 Fancier version of deprecated ``newline_with_copy_margin`` which should
618 520 compute the correct indentation of the inserted line. That is to say, indent
619 521 by 4 extra space after a function definition, class definition, context
620 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):
624 """Insert a newline after the cursor indented appropriately."""
625 b = event.current_buffer
626 d = b.document
627
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
529 if b.complete_state:
530 b.cancel_completion()
531 text = d.text[: d.cursor_position] + "\n"
532 _, indent = inputsplitter.check_complete(text)
533 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
637 534
638 535
639 536 def open_input_in_editor(event):
@@ -666,5 +563,58 b' else:'
666 563
667 564 @undoc
668 565 def win_paste(event):
669 """Stub used when auto-generating shortcuts for documentation"""
566 """Stub used on other platforms"""
670 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 5 like whether we are in edit mode, and whether the completer is open.
6 6 """
7 7 import re
@@ -84,9 +84,9 b' def raw_string_braces(event: KeyPressEvent):'
84 84
85 85
86 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 90 event.current_buffer.cursor_right()
91 91
92 92
@@ -1,7 +1,7 b''
1 1 import re
2 2 import tokenize
3 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 6 from prompt_toolkit.buffer import Buffer
7 7 from prompt_toolkit.key_binding import KeyPressEvent
@@ -16,6 +16,7 b' from prompt_toolkit.layout.processors import ('
16 16 TransformationInput,
17 17 )
18 18
19 from IPython.core.getipython import get_ipython
19 20 from IPython.utils.tokenutil import generate_tokens
20 21
21 22
@@ -346,33 +347,29 b' def _swap_autosuggestion('
346 347 buffer.suggestion = new_suggestion
347 348
348 349
349 def swap_autosuggestion_up(provider: Provider):
350 def swap_autosuggestion_up(event: KeyPressEvent):
351 """Get next autosuggestion from history."""
352 if not isinstance(provider, NavigableAutoSuggestFromHistory):
353 return
350 def swap_autosuggestion_up(event: KeyPressEvent):
351 """Get next autosuggestion from history."""
352 shell = get_ipython()
353 provider = shell.auto_suggest
354 354
355 return _swap_autosuggestion(
356 buffer=event.current_buffer, provider=provider, direction_method=provider.up
357 )
355 if not isinstance(provider, NavigableAutoSuggestFromHistory):
356 return
358 357
359 swap_autosuggestion_up.__name__ = "swap_autosuggestion_up"
360 return swap_autosuggestion_up
358 return _swap_autosuggestion(
359 buffer=event.current_buffer, provider=provider, direction_method=provider.up
360 )
361 361
362 362
363 def swap_autosuggestion_down(
364 provider: Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
365 ):
366 def swap_autosuggestion_down(event: KeyPressEvent):
367 """Get previous autosuggestion from history."""
368 if not isinstance(provider, NavigableAutoSuggestFromHistory):
369 return
363 def swap_autosuggestion_down(event: KeyPressEvent):
364 """Get previous autosuggestion from history."""
365 shell = get_ipython()
366 provider = shell.auto_suggest
370 367
371 return _swap_autosuggestion(
372 buffer=event.current_buffer,
373 provider=provider,
374 direction_method=provider.down,
375 )
368 if not isinstance(provider, NavigableAutoSuggestFromHistory):
369 return
376 370
377 swap_autosuggestion_down.__name__ = "swap_autosuggestion_down"
378 return swap_autosuggestion_down
371 return _swap_autosuggestion(
372 buffer=event.current_buffer,
373 provider=provider,
374 direction_method=provider.down,
375 )
@@ -9,9 +9,8 b' import os'
9 9
10 10 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
11 11
12 from IPython.core.inputtransformer import InputTransformer
12
13 13 from IPython.testing import tools as tt
14 from IPython.utils.capture import capture_output
15 14
16 15 from IPython.terminal.ptutils import _elide, _adjust_completion_text_based_on_context
17 16 from IPython.terminal.shortcuts.auto_suggest import NavigableAutoSuggestFromHistory
@@ -11,6 +11,8 b' from IPython.terminal.shortcuts.auto_suggest import ('
11 11 swap_autosuggestion_up,
12 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 17 from prompt_toolkit.history import InMemoryHistory
16 18 from prompt_toolkit.buffer import Buffer
@@ -192,18 +194,20 b' def test_autosuggest_token_empty():'
192 194 def test_other_providers():
193 195 """Ensure that swapping autosuggestions does not break with other providers"""
194 196 provider = AutoSuggestFromHistory()
195 up = swap_autosuggestion_up(provider)
196 down = swap_autosuggestion_down(provider)
197 ip = get_ipython()
198 ip.auto_suggest = provider
197 199 event = Mock()
198 200 event.current_buffer = Buffer()
199 assert up(event) is None
200 assert down(event) is None
201 assert swap_autosuggestion_up(event) is None
202 assert swap_autosuggestion_down(event) is None
201 203
202 204
203 205 async def test_navigable_provider():
204 206 provider = NavigableAutoSuggestFromHistory()
205 207 history = InMemoryHistory(history_strings=["very_a", "very", "very_b", "very_c"])
206 208 buffer = Buffer(history=history)
209 ip = get_ipython()
210 ip.auto_suggest = provider
207 211
208 212 async for _ in history.load():
209 213 pass
@@ -211,8 +215,8 b' async def test_navigable_provider():'
211 215 buffer.cursor_position = 5
212 216 buffer.text = "very"
213 217
214 up = swap_autosuggestion_up(provider)
215 down = swap_autosuggestion_down(provider)
218 up = swap_autosuggestion_up
219 down = swap_autosuggestion_down
216 220
217 221 event = Mock()
218 222 event.current_buffer = buffer
@@ -254,14 +258,16 b' async def test_navigable_provider_multiline_entries():'
254 258 provider = NavigableAutoSuggestFromHistory()
255 259 history = InMemoryHistory(history_strings=["very_a\nvery_b", "very_c"])
256 260 buffer = Buffer(history=history)
261 ip = get_ipython()
262 ip.auto_suggest = provider
257 263
258 264 async for _ in history.load():
259 265 pass
260 266
261 267 buffer.cursor_position = 5
262 268 buffer.text = "very"
263 up = swap_autosuggestion_up(provider)
264 down = swap_autosuggestion_down(provider)
269 up = swap_autosuggestion_up
270 down = swap_autosuggestion_down
265 271
266 272 event = Mock()
267 273 event.current_buffer = buffer
@@ -316,3 +322,101 b' def test_navigable_provider_connection():'
316 322 session_1.default_buffer.on_text_insert.fire()
317 323 session_2.default_buffer.on_text_insert.fire()
318 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 1 from dataclasses import dataclass
2 2 from inspect import getsource
3 3 from pathlib import Path
4 from typing import cast, Callable, List, Union
4 from typing import cast, List, Union
5 5 from html import escape as html_escape
6 6 import re
7 7
@@ -10,7 +10,8 b' from prompt_toolkit.key_binding import KeyBindingsBase'
10 10 from prompt_toolkit.filters import Filter, Condition
11 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 17 @dataclass
@@ -44,11 +45,16 b' class _Invert(Filter):'
44 45 filter: Filter
45 46
46 47
47 conjunctions_labels = {"_AndList": "and", "_OrList": "or"}
48 conjunctions_labels = {"_AndList": "&", "_OrList": "|"}
48 49
49 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 58 def format_filter(
53 59 filter_: Union[Filter, _NestedFilter, Condition, _Invert],
54 60 is_top_level=True,
@@ -58,6 +64,8 b' def format_filter('
58 64 s = filter_.__class__.__name__
59 65 if s == "Condition":
60 66 func = cast(Condition, filter_).func
67 if filter_ in HUMAN_NAMES_FOR_FILTERS:
68 return HUMAN_NAMES_FOR_FILTERS[filter_]
61 69 name = func.__name__
62 70 if name == "<lambda>":
63 71 source = getsource(func)
@@ -66,10 +74,12 b' def format_filter('
66 74 elif s == "_Invert":
67 75 operand = cast(_Invert, filter_).filter
68 76 if operand.__class__.__name__ in ATOMIC_CLASSES:
69 return f"not {format_filter(operand, is_top_level=False)}"
70 return f"not ({format_filter(operand, is_top_level=False)})"
77 return f"~{format_filter(operand, is_top_level=False)}"
78 return f"~({format_filter(operand, is_top_level=False)})"
71 79 elif s in conjunctions_labels:
72 80 filters = cast(_NestedFilter, filter_).filters
81 if filter_ in HUMAN_NAMES_FOR_FILTERS:
82 return HUMAN_NAMES_FOR_FILTERS[filter_]
73 83 conjunction = conjunctions_labels[s]
74 84 glue = f" {conjunction} "
75 85 result = glue.join(format_filter(x, is_top_level=False) for x in filters)
@@ -104,17 +114,6 b' class _DummyTerminal:'
104 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 117 def bindings_from_prompt_toolkit(prompt_bindings: KeyBindingsBase) -> List[Binding]:
119 118 """Collect bindings to a simple format that does not depend on prompt-toolkit internals"""
120 119 bindings: List[Binding] = []
@@ -178,11 +177,12 b' def format_prompt_keys(keys: str, add_alternatives=True) -> str:'
178 177
179 178 return result
180 179
180
181 181 if __name__ == '__main__':
182 182 here = Path(__file__).parent
183 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 187 session = PromptSession(key_bindings=ipy_bindings)
188 188 prompt_bindings = session.app.key_bindings
General Comments 0
You need to be logged in to leave comments. Login now