Show More
@@ -0,0 +1,90 b'' | |||||
|
1 | import re | |||
|
2 | from prompt_toolkit.key_binding import KeyPressEvent | |||
|
3 | ||||
|
4 | ||||
|
5 | def parenthesis(event: KeyPressEvent): | |||
|
6 | """Auto-close parenthesis""" | |||
|
7 | event.current_buffer.insert_text("()") | |||
|
8 | event.current_buffer.cursor_left() | |||
|
9 | ||||
|
10 | ||||
|
11 | def brackets(event: KeyPressEvent): | |||
|
12 | """Auto-close brackets""" | |||
|
13 | event.current_buffer.insert_text("[]") | |||
|
14 | event.current_buffer.cursor_left() | |||
|
15 | ||||
|
16 | ||||
|
17 | def braces(event: KeyPressEvent): | |||
|
18 | """Auto-close braces""" | |||
|
19 | event.current_buffer.insert_text("{}") | |||
|
20 | event.current_buffer.cursor_left() | |||
|
21 | ||||
|
22 | ||||
|
23 | def double_quote(event: KeyPressEvent): | |||
|
24 | """Auto-close double quotes""" | |||
|
25 | event.current_buffer.insert_text('""') | |||
|
26 | event.current_buffer.cursor_left() | |||
|
27 | ||||
|
28 | ||||
|
29 | def single_quote(event: KeyPressEvent): | |||
|
30 | """Auto-close single quotes""" | |||
|
31 | event.current_buffer.insert_text("''") | |||
|
32 | event.current_buffer.cursor_left() | |||
|
33 | ||||
|
34 | ||||
|
35 | def docstring_double_quotes(event: KeyPressEvent): | |||
|
36 | """Auto-close docstring (double quotes)""" | |||
|
37 | event.current_buffer.insert_text('""""') | |||
|
38 | event.current_buffer.cursor_left(3) | |||
|
39 | ||||
|
40 | ||||
|
41 | def docstring_single_quotes(event: KeyPressEvent): | |||
|
42 | """Auto-close docstring (single quotes)""" | |||
|
43 | event.current_buffer.insert_text("''''") | |||
|
44 | event.current_buffer.cursor_left(3) | |||
|
45 | ||||
|
46 | ||||
|
47 | def raw_string_parenthesis(event: KeyPressEvent): | |||
|
48 | """Auto-close parenthesis in raw strings""" | |||
|
49 | matches = re.match( | |||
|
50 | r".*(r|R)[\"'](-*)", | |||
|
51 | event.current_buffer.document.current_line_before_cursor, | |||
|
52 | ) | |||
|
53 | dashes = matches.group(2) or "" | |||
|
54 | event.current_buffer.insert_text("()" + dashes) | |||
|
55 | event.current_buffer.cursor_left(len(dashes) + 1) | |||
|
56 | ||||
|
57 | ||||
|
58 | def raw_string_bracket(event: KeyPressEvent): | |||
|
59 | """Auto-close bracker in raw strings""" | |||
|
60 | matches = re.match( | |||
|
61 | r".*(r|R)[\"'](-*)", | |||
|
62 | event.current_buffer.document.current_line_before_cursor, | |||
|
63 | ) | |||
|
64 | dashes = matches.group(2) or "" | |||
|
65 | event.current_buffer.insert_text("[]" + dashes) | |||
|
66 | event.current_buffer.cursor_left(len(dashes) + 1) | |||
|
67 | ||||
|
68 | ||||
|
69 | def raw_string_braces(event: KeyPressEvent): | |||
|
70 | """Auto-close braces in raw strings""" | |||
|
71 | matches = re.match( | |||
|
72 | r".*(r|R)[\"'](-*)", | |||
|
73 | event.current_buffer.document.current_line_before_cursor, | |||
|
74 | ) | |||
|
75 | dashes = matches.group(2) or "" | |||
|
76 | event.current_buffer.insert_text("{}" + dashes) | |||
|
77 | event.current_buffer.cursor_left(len(dashes) + 1) | |||
|
78 | ||||
|
79 | ||||
|
80 | def skip_over(event: KeyPressEvent): | |||
|
81 | """Skip over automatically added parenthesis. | |||
|
82 | ||||
|
83 | (rather than adding another parenthesis)""" | |||
|
84 | event.current_buffer.cursor_right() | |||
|
85 | ||||
|
86 | ||||
|
87 | def delete_pair(event: KeyPressEvent): | |||
|
88 | """Delete auto-closed parenthesis""" | |||
|
89 | event.current_buffer.delete() | |||
|
90 | event.current_buffer.delete_before_cursor() |
@@ -0,0 +1,39 b'' | |||||
|
1 | import re | |||
|
2 | from prompt_toolkit.key_binding import KeyPressEvent | |||
|
3 | from prompt_toolkit.key_binding.bindings import named_commands as nc | |||
|
4 | ||||
|
5 | ||||
|
6 | # Needed for to accept autosuggestions in vi insert mode | |||
|
7 | def accept_in_vi_insert_mode(event: KeyPressEvent): | |||
|
8 | """Apply autosuggestion if at end of line.""" | |||
|
9 | b = event.current_buffer | |||
|
10 | d = b.document | |||
|
11 | after_cursor = d.text[d.cursor_position :] | |||
|
12 | lines = after_cursor.split("\n") | |||
|
13 | end_of_current_line = lines[0].strip() | |||
|
14 | suggestion = b.suggestion | |||
|
15 | if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""): | |||
|
16 | b.insert_text(suggestion.text) | |||
|
17 | else: | |||
|
18 | nc.end_of_line(event) | |||
|
19 | ||||
|
20 | ||||
|
21 | def accept(event): | |||
|
22 | """Accept suggestion""" | |||
|
23 | b = event.current_buffer | |||
|
24 | suggestion = b.suggestion | |||
|
25 | if suggestion: | |||
|
26 | b.insert_text(suggestion.text) | |||
|
27 | else: | |||
|
28 | nc.forward_char(event) | |||
|
29 | ||||
|
30 | ||||
|
31 | def accept_word(event): | |||
|
32 | """Fill partial suggestion by word""" | |||
|
33 | b = event.current_buffer | |||
|
34 | suggestion = b.suggestion | |||
|
35 | if suggestion: | |||
|
36 | t = re.split(r"(\S+\s+)", suggestion.text) | |||
|
37 | b.insert_text(next((x for x in t if x), "")) | |||
|
38 | else: | |||
|
39 | nc.forward_word(event) |
@@ -0,0 +1,7 b'' | |||||
|
1 | /* | |||
|
2 | Needed to revert problematic lack of wrapping in sphinx_rtd_theme, see: | |||
|
3 | https://github.com/readthedocs/sphinx_rtd_theme/issues/117 | |||
|
4 | */ | |||
|
5 | .wy-table-responsive table.shortcuts td, .wy-table-responsive table.shortcuts th { | |||
|
6 | white-space: normal!important; | |||
|
7 | } |
@@ -16,14 +16,29 b' from typing import Callable' | |||||
16 |
|
16 | |||
17 | from prompt_toolkit.application.current import get_app |
|
17 | from prompt_toolkit.application.current import get_app | |
18 | from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER |
|
18 | from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER | |
19 |
from prompt_toolkit.filters import ( |
|
19 | from prompt_toolkit.filters import ( | |
20 | vi_insert_mode, emacs_insert_mode, has_completions, vi_mode) |
|
20 | has_focus as has_focus_impl, | |
21 | from prompt_toolkit.key_binding.bindings.completion import display_completions_like_readline |
|
21 | has_selection, | |
|
22 | Condition, | |||
|
23 | vi_insert_mode, | |||
|
24 | emacs_insert_mode, | |||
|
25 | has_completions, | |||
|
26 | vi_mode, | |||
|
27 | ) | |||
|
28 | from prompt_toolkit.key_binding.bindings.completion import ( | |||
|
29 | display_completions_like_readline, | |||
|
30 | ) | |||
22 | from prompt_toolkit.key_binding import KeyBindings |
|
31 | from prompt_toolkit.key_binding import KeyBindings | |
23 | from prompt_toolkit.key_binding.bindings import named_commands as nc |
|
32 | from prompt_toolkit.key_binding.bindings import named_commands as nc | |
24 | from prompt_toolkit.key_binding.vi_state import InputMode, ViState |
|
33 | from prompt_toolkit.key_binding.vi_state import InputMode, ViState | |
|
34 | from prompt_toolkit.layout.layout import FocusableElement | |||
25 |
|
35 | |||
26 | from IPython.utils.decorators import undoc |
|
36 | from IPython.utils.decorators import undoc | |
|
37 | from . import auto_match as match, autosuggestions | |||
|
38 | ||||
|
39 | ||||
|
40 | __all__ = ["create_ipython_shortcuts"] | |||
|
41 | ||||
27 |
|
42 | |||
28 | @undoc |
|
43 | @undoc | |
29 | @Condition |
|
44 | @Condition | |
@@ -32,80 +47,84 b' def cursor_in_leading_ws():' | |||||
32 | return (not before) or before.isspace() |
|
47 | return (not before) or before.isspace() | |
33 |
|
48 | |||
34 |
|
49 | |||
35 | # Needed for to accept autosuggestions in vi insert mode |
|
50 | def has_focus(value: FocusableElement): | |
36 | def _apply_autosuggest(event): |
|
51 | """Wrapper around has_focus adding a nice `__name__` to tester function""" | |
37 | """ |
|
52 | tester = has_focus_impl(value).func | |
38 | Apply autosuggestion if at end of line. |
|
53 | tester.__name__ = f"is_focused({value})" | |
39 | """ |
|
54 | return Condition(tester) | |
40 | b = event.current_buffer |
|
|||
41 | d = b.document |
|
|||
42 | after_cursor = d.text[d.cursor_position :] |
|
|||
43 | lines = after_cursor.split("\n") |
|
|||
44 | end_of_current_line = lines[0].strip() |
|
|||
45 | suggestion = b.suggestion |
|
|||
46 | if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""): |
|
|||
47 | b.insert_text(suggestion.text) |
|
|||
48 | else: |
|
|||
49 | nc.end_of_line(event) |
|
|||
50 |
|
55 | |||
51 | def create_ipython_shortcuts(shell): |
|
56 | ||
52 | """Set up the prompt_toolkit keyboard shortcuts for IPython""" |
|
57 | def create_ipython_shortcuts(shell, for_all_platforms: bool = False): | |
|
58 | """Set up the prompt_toolkit keyboard shortcuts for IPython.""" | |||
|
59 | # Warning: if possible, do NOT define handler functions in the locals | |||
|
60 | # scope of this function, instead define functions in the global | |||
|
61 | # scope, or a separate module, and include a user-friendly docstring | |||
|
62 | # describing the action. | |||
53 |
|
63 | |||
54 | kb = KeyBindings() |
|
64 | kb = KeyBindings() | |
55 | insert_mode = vi_insert_mode | emacs_insert_mode |
|
65 | insert_mode = vi_insert_mode | emacs_insert_mode | |
56 |
|
66 | |||
57 |
if getattr(shell, |
|
67 | if getattr(shell, "handle_return", None): | |
58 | return_handler = shell.handle_return(shell) |
|
68 | return_handler = shell.handle_return(shell) | |
59 | else: |
|
69 | else: | |
60 | return_handler = newline_or_execute_outer(shell) |
|
70 | return_handler = newline_or_execute_outer(shell) | |
61 |
|
71 | |||
62 |
kb.add( |
|
72 | kb.add("enter", filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode))( | |
63 | & ~has_selection |
|
73 | return_handler | |
64 | & insert_mode |
|
74 | ) | |
65 | ))(return_handler) |
|
|||
66 |
|
||||
67 | def reformat_and_execute(event): |
|
|||
68 | reformat_text_before_cursor(event.current_buffer, event.current_buffer.document, shell) |
|
|||
69 | event.current_buffer.validate_and_handle() |
|
|||
70 |
|
75 | |||
71 | @Condition |
|
76 | @Condition | |
72 | def ebivim(): |
|
77 | def ebivim(): | |
73 | return shell.emacs_bindings_in_vi_insert_mode |
|
78 | return shell.emacs_bindings_in_vi_insert_mode | |
74 |
|
79 | |||
75 | kb.add( |
|
80 | @kb.add( | |
76 | "escape", |
|
81 | "escape", | |
77 | "enter", |
|
82 | "enter", | |
78 | filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode & ebivim), |
|
83 | filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode & ebivim), | |
79 | )(reformat_and_execute) |
|
84 | ) | |
|
85 | def reformat_and_execute(event): | |||
|
86 | """Reformat code and execute it""" | |||
|
87 | reformat_text_before_cursor( | |||
|
88 | event.current_buffer, event.current_buffer.document, shell | |||
|
89 | ) | |||
|
90 | event.current_buffer.validate_and_handle() | |||
80 |
|
91 | |||
81 | kb.add("c-\\")(quit) |
|
92 | kb.add("c-\\")(quit) | |
82 |
|
93 | |||
83 |
kb.add( |
|
94 | kb.add("c-p", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))( | |
84 |
|
|
95 | previous_history_or_previous_completion | |
|
96 | ) | |||
85 |
|
97 | |||
86 |
kb.add( |
|
98 | kb.add("c-n", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))( | |
87 |
|
|
99 | next_history_or_next_completion | |
|
100 | ) | |||
88 |
|
101 | |||
89 |
kb.add( |
|
102 | kb.add("c-g", filter=(has_focus(DEFAULT_BUFFER) & has_completions))( | |
90 |
|
|
103 | dismiss_completion | |
|
104 | ) | |||
91 |
|
105 | |||
92 |
kb.add( |
|
106 | kb.add("c-c", filter=has_focus(DEFAULT_BUFFER))(reset_buffer) | |
93 |
|
107 | |||
94 |
kb.add( |
|
108 | kb.add("c-c", filter=has_focus(SEARCH_BUFFER))(reset_search_buffer) | |
95 |
|
109 | |||
96 |
supports_suspend = Condition(lambda: hasattr(signal, |
|
110 | supports_suspend = Condition(lambda: hasattr(signal, "SIGTSTP")) | |
97 |
kb.add( |
|
111 | kb.add("c-z", filter=supports_suspend)(suspend_to_bg) | |
98 |
|
112 | |||
99 | # Ctrl+I == Tab |
|
113 | # Ctrl+I == Tab | |
100 | kb.add('tab', filter=(has_focus(DEFAULT_BUFFER) |
|
114 | kb.add( | |
101 | & ~has_selection |
|
115 | "tab", | |
102 | & insert_mode |
|
116 | filter=( | |
103 | & cursor_in_leading_ws |
|
117 | has_focus(DEFAULT_BUFFER) | |
104 | ))(indent_buffer) |
|
118 | & ~has_selection | |
105 | kb.add('c-o', filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode) |
|
119 | & insert_mode | |
106 | )(newline_autoindent_outer(shell.input_transformer_manager)) |
|
120 | & cursor_in_leading_ws | |
|
121 | ), | |||
|
122 | )(indent_buffer) | |||
|
123 | kb.add("c-o", filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode))( | |||
|
124 | newline_autoindent_outer(shell.input_transformer_manager) | |||
|
125 | ) | |||
107 |
|
126 | |||
108 |
kb.add( |
|
127 | kb.add("f2", filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor) | |
109 |
|
128 | |||
110 | @Condition |
|
129 | @Condition | |
111 | def auto_match(): |
|
130 | def auto_match(): | |
@@ -146,6 +165,8 b' def create_ipython_shortcuts(shell):' | |||||
146 | before_cursor = app.current_buffer.document.current_line_before_cursor |
|
165 | before_cursor = app.current_buffer.document.current_line_before_cursor | |
147 | return bool(m.match(before_cursor)) |
|
166 | return bool(m.match(before_cursor)) | |
148 |
|
167 | |||
|
168 | _preceding_text.__name__ = f"preceding_text({pattern!r})" | |||
|
169 | ||||
149 | condition = Condition(_preceding_text) |
|
170 | condition = Condition(_preceding_text) | |
150 | _preceding_text_cache[pattern] = condition |
|
171 | _preceding_text_cache[pattern] = condition | |
151 | return condition |
|
172 | return condition | |
@@ -161,6 +182,8 b' def create_ipython_shortcuts(shell):' | |||||
161 | app = get_app() |
|
182 | app = get_app() | |
162 | return bool(m.match(app.current_buffer.document.current_line_after_cursor)) |
|
183 | return bool(m.match(app.current_buffer.document.current_line_after_cursor)) | |
163 |
|
184 | |||
|
185 | _following_text.__name__ = f"following_text({pattern!r})" | |||
|
186 | ||||
164 | condition = Condition(_following_text) |
|
187 | condition = Condition(_following_text) | |
165 | _following_text_cache[pattern] = condition |
|
188 | _following_text_cache[pattern] = condition | |
166 | return condition |
|
189 | return condition | |
@@ -178,151 +201,110 b' def create_ipython_shortcuts(shell):' | |||||
178 | return not ('"' in s or "'" in s) |
|
201 | return not ('"' in s or "'" in s) | |
179 |
|
202 | |||
180 | # auto match |
|
203 | # auto match | |
181 | @kb.add("(", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) |
|
204 | auto_match_parens = {"(": match.parenthesis, "[": match.brackets, "{": match.braces} | |
182 | def _(event): |
|
205 | for key, cmd in auto_match_parens.items(): | |
183 | event.current_buffer.insert_text("()") |
|
206 | kb.add(key, filter=focused_insert & auto_match & following_text(r"[,)}\]]|$"))( | |
184 | event.current_buffer.cursor_left() |
|
207 | cmd | |
185 |
|
208 | ) | ||
186 | @kb.add("[", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) |
|
|||
187 | def _(event): |
|
|||
188 | event.current_buffer.insert_text("[]") |
|
|||
189 | event.current_buffer.cursor_left() |
|
|||
190 |
|
||||
191 | @kb.add("{", filter=focused_insert & auto_match & following_text(r"[,)}\]]|$")) |
|
|||
192 | def _(event): |
|
|||
193 | event.current_buffer.insert_text("{}") |
|
|||
194 | event.current_buffer.cursor_left() |
|
|||
195 |
|
209 | |||
196 |
|
|
210 | kb.add( | |
197 | '"', |
|
211 | '"', | |
198 | filter=focused_insert |
|
212 | filter=focused_insert | |
199 | & auto_match |
|
213 | & auto_match | |
200 | & not_inside_unclosed_string |
|
214 | & not_inside_unclosed_string | |
201 | & preceding_text(lambda line: all_quotes_paired('"', line)) |
|
215 | & preceding_text(lambda line: all_quotes_paired('"', line)) | |
202 | & following_text(r"[,)}\]]|$"), |
|
216 | & following_text(r"[,)}\]]|$"), | |
203 | ) |
|
217 | )(match.double_quote) | |
204 | def _(event): |
|
|||
205 | event.current_buffer.insert_text('""') |
|
|||
206 | event.current_buffer.cursor_left() |
|
|||
207 |
|
218 | |||
208 |
|
|
219 | kb.add( | |
209 | "'", |
|
220 | "'", | |
210 | filter=focused_insert |
|
221 | filter=focused_insert | |
211 | & auto_match |
|
222 | & auto_match | |
212 | & not_inside_unclosed_string |
|
223 | & not_inside_unclosed_string | |
213 | & preceding_text(lambda line: all_quotes_paired("'", line)) |
|
224 | & preceding_text(lambda line: all_quotes_paired("'", line)) | |
214 | & following_text(r"[,)}\]]|$"), |
|
225 | & following_text(r"[,)}\]]|$"), | |
215 | ) |
|
226 | )(match.single_quote) | |
216 | def _(event): |
|
|||
217 | event.current_buffer.insert_text("''") |
|
|||
218 | event.current_buffer.cursor_left() |
|
|||
219 |
|
227 | |||
220 |
|
|
228 | kb.add( | |
221 | '"', |
|
229 | '"', | |
222 | filter=focused_insert |
|
230 | filter=focused_insert | |
223 | & auto_match |
|
231 | & auto_match | |
224 | & not_inside_unclosed_string |
|
232 | & not_inside_unclosed_string | |
225 | & preceding_text(r'^.*""$'), |
|
233 | & preceding_text(r'^.*""$'), | |
226 | ) |
|
234 | )(match.docstring_double_quotes) | |
227 | def _(event): |
|
|||
228 | event.current_buffer.insert_text('""""') |
|
|||
229 | event.current_buffer.cursor_left(3) |
|
|||
230 |
|
235 | |||
231 |
|
|
236 | kb.add( | |
232 | "'", |
|
237 | "'", | |
233 | filter=focused_insert |
|
238 | filter=focused_insert | |
234 | & auto_match |
|
239 | & auto_match | |
235 | & not_inside_unclosed_string |
|
240 | & not_inside_unclosed_string | |
236 | & preceding_text(r"^.*''$"), |
|
241 | & preceding_text(r"^.*''$"), | |
237 | ) |
|
242 | )(match.docstring_single_quotes) | |
238 | def _(event): |
|
|||
239 | event.current_buffer.insert_text("''''") |
|
|||
240 | event.current_buffer.cursor_left(3) |
|
|||
241 |
|
243 | |||
242 | # raw string |
|
244 | # raw string | |
243 | @kb.add( |
|
245 | auto_match_parens_raw_string = { | |
244 | "(", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") |
|
246 | "(": match.raw_string_parenthesis, | |
245 | ) |
|
247 | "[": match.raw_string_bracket, | |
246 | def _(event): |
|
248 | "{": match.raw_string_braces, | |
247 | matches = re.match( |
|
249 | } | |
248 | r".*(r|R)[\"'](-*)", |
|
250 | for key, cmd in auto_match_parens_raw_string.items(): | |
249 | event.current_buffer.document.current_line_before_cursor, |
|
251 | kb.add( | |
250 |
|
|
252 | key, | |
251 | dashes = matches.group(2) or "" |
|
253 | filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$"), | |
252 | event.current_buffer.insert_text("()" + dashes) |
|
254 | )(cmd) | |
253 | event.current_buffer.cursor_left(len(dashes) + 1) |
|
|||
254 |
|
255 | |||
255 | @kb.add( |
|
256 | # just move cursor | |
256 |
|
|
257 | kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)"))( | |
|
258 | match.skip_over | |||
257 | ) |
|
259 | ) | |
258 | def _(event): |
|
260 | kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]"))( | |
259 |
match |
|
261 | match.skip_over | |
260 | r".*(r|R)[\"'](-*)", |
|
262 | ) | |
261 | event.current_buffer.document.current_line_before_cursor, |
|
263 | kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}"))( | |
262 | ) |
|
264 | match.skip_over | |
263 | dashes = matches.group(2) or "" |
|
265 | ) | |
264 | event.current_buffer.insert_text("[]" + dashes) |
|
266 | kb.add('"', filter=focused_insert & auto_match & following_text('^"'))( | |
265 | event.current_buffer.cursor_left(len(dashes) + 1) |
|
267 | match.skip_over | |
266 |
|
268 | ) | ||
267 | @kb.add( |
|
269 | kb.add("'", filter=focused_insert & auto_match & following_text("^'"))( | |
268 | "{", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$") |
|
270 | match.skip_over | |
269 | ) |
|
271 | ) | |
270 | def _(event): |
|
|||
271 | matches = re.match( |
|
|||
272 | r".*(r|R)[\"'](-*)", |
|
|||
273 | event.current_buffer.document.current_line_before_cursor, |
|
|||
274 | ) |
|
|||
275 | dashes = matches.group(2) or "" |
|
|||
276 | event.current_buffer.insert_text("{}" + dashes) |
|
|||
277 | event.current_buffer.cursor_left(len(dashes) + 1) |
|
|||
278 |
|
||||
279 | # just move cursor |
|
|||
280 | @kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)")) |
|
|||
281 | @kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]")) |
|
|||
282 | @kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}")) |
|
|||
283 | @kb.add('"', filter=focused_insert & auto_match & following_text('^"')) |
|
|||
284 | @kb.add("'", filter=focused_insert & auto_match & following_text("^'")) |
|
|||
285 | def _(event): |
|
|||
286 | event.current_buffer.cursor_right() |
|
|||
287 |
|
272 | |||
288 |
|
|
273 | kb.add( | |
289 | "backspace", |
|
274 | "backspace", | |
290 | filter=focused_insert |
|
275 | filter=focused_insert | |
291 | & preceding_text(r".*\($") |
|
276 | & preceding_text(r".*\($") | |
292 | & auto_match |
|
277 | & auto_match | |
293 | & following_text(r"^\)"), |
|
278 | & following_text(r"^\)"), | |
294 | ) |
|
279 | )(match.delete_pair) | |
295 |
|
|
280 | kb.add( | |
296 | "backspace", |
|
281 | "backspace", | |
297 | filter=focused_insert |
|
282 | filter=focused_insert | |
298 | & preceding_text(r".*\[$") |
|
283 | & preceding_text(r".*\[$") | |
299 | & auto_match |
|
284 | & auto_match | |
300 | & following_text(r"^\]"), |
|
285 | & following_text(r"^\]"), | |
301 | ) |
|
286 | )(match.delete_pair) | |
302 |
|
|
287 | kb.add( | |
303 | "backspace", |
|
288 | "backspace", | |
304 | filter=focused_insert |
|
289 | filter=focused_insert | |
305 | & preceding_text(r".*\{$") |
|
290 | & preceding_text(r".*\{$") | |
306 | & auto_match |
|
291 | & auto_match | |
307 | & following_text(r"^\}"), |
|
292 | & following_text(r"^\}"), | |
308 | ) |
|
293 | )(match.delete_pair) | |
309 |
|
|
294 | kb.add( | |
310 | "backspace", |
|
295 | "backspace", | |
311 | filter=focused_insert |
|
296 | filter=focused_insert | |
312 | & preceding_text('.*"$') |
|
297 | & preceding_text('.*"$') | |
313 | & auto_match |
|
298 | & auto_match | |
314 | & following_text('^"'), |
|
299 | & following_text('^"'), | |
315 | ) |
|
300 | )(match.delete_pair) | |
316 |
|
|
301 | kb.add( | |
317 | "backspace", |
|
302 | "backspace", | |
318 | filter=focused_insert |
|
303 | filter=focused_insert | |
319 | & preceding_text(r".*'$") |
|
304 | & preceding_text(r".*'$") | |
320 | & auto_match |
|
305 | & auto_match | |
321 | & following_text(r"^'"), |
|
306 | & following_text(r"^'"), | |
322 | ) |
|
307 | )(match.delete_pair) | |
323 | def _(event): |
|
|||
324 | event.current_buffer.delete() |
|
|||
325 | event.current_buffer.delete_before_cursor() |
|
|||
326 |
|
308 | |||
327 | if shell.display_completions == "readlinelike": |
|
309 | if shell.display_completions == "readlinelike": | |
328 | kb.add( |
|
310 | kb.add( | |
@@ -335,37 +317,22 b' def create_ipython_shortcuts(shell):' | |||||
335 | ), |
|
317 | ), | |
336 | )(display_completions_like_readline) |
|
318 | )(display_completions_like_readline) | |
337 |
|
319 | |||
338 | if sys.platform == "win32": |
|
320 | if sys.platform == "win32" or for_all_platforms: | |
339 | kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste) |
|
321 | kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste) | |
340 |
|
322 | |||
341 | focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode |
|
323 | focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode | |
342 |
|
324 | |||
343 | @kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode)) |
|
325 | # autosuggestions | |
344 | def _(event): |
|
326 | kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))( | |
345 | _apply_autosuggest(event) |
|
327 | autosuggestions.accept_in_vi_insert_mode | |
346 |
|
328 | ) | ||
347 |
|
|
329 | kb.add("c-e", filter=focused_insert_vi & ebivim)( | |
348 | def _(event): |
|
330 | autosuggestions.accept_in_vi_insert_mode | |
349 | _apply_autosuggest(event) |
|
331 | ) | |
350 |
|
332 | kb.add("c-f", filter=focused_insert_vi)(autosuggestions.accept) | ||
351 |
|
|
333 | kb.add("escape", "f", filter=focused_insert_vi & ebivim)( | |
352 | def _(event): |
|
334 | autosuggestions.accept_word | |
353 | b = event.current_buffer |
|
335 | ) | |
354 | suggestion = b.suggestion |
|
|||
355 | if suggestion: |
|
|||
356 | b.insert_text(suggestion.text) |
|
|||
357 | else: |
|
|||
358 | nc.forward_char(event) |
|
|||
359 |
|
||||
360 | @kb.add("escape", "f", filter=focused_insert_vi & ebivim) |
|
|||
361 | def _(event): |
|
|||
362 | b = event.current_buffer |
|
|||
363 | suggestion = b.suggestion |
|
|||
364 | if suggestion: |
|
|||
365 | t = re.split(r"(\S+\s+)", suggestion.text) |
|
|||
366 | b.insert_text(next((x for x in t if x), "")) |
|
|||
367 | else: |
|
|||
368 | nc.forward_word(event) |
|
|||
369 |
|
336 | |||
370 | # Simple Control keybindings |
|
337 | # Simple Control keybindings | |
371 | key_cmd_dict = { |
|
338 | key_cmd_dict = { | |
@@ -423,7 +390,7 b' def create_ipython_shortcuts(shell):' | |||||
423 |
|
390 | |||
424 |
|
391 | |||
425 | def reformat_text_before_cursor(buffer, document, shell): |
|
392 | def reformat_text_before_cursor(buffer, document, shell): | |
426 | text = buffer.delete_before_cursor(len(document.text[:document.cursor_position])) |
|
393 | text = buffer.delete_before_cursor(len(document.text[: document.cursor_position])) | |
427 | try: |
|
394 | try: | |
428 | formatted_text = shell.reformat_handler(text) |
|
395 | formatted_text = shell.reformat_handler(text) | |
429 | buffer.insert_text(formatted_text) |
|
396 | buffer.insert_text(formatted_text) | |
@@ -432,7 +399,6 b' def reformat_text_before_cursor(buffer, document, shell):' | |||||
432 |
|
399 | |||
433 |
|
400 | |||
434 | def newline_or_execute_outer(shell): |
|
401 | def newline_or_execute_outer(shell): | |
435 |
|
||||
436 | def newline_or_execute(event): |
|
402 | def newline_or_execute(event): | |
437 | """When the user presses return, insert a newline or execute the code.""" |
|
403 | """When the user presses return, insert a newline or execute the code.""" | |
438 | b = event.current_buffer |
|
404 | b = event.current_buffer | |
@@ -451,34 +417,38 b' def newline_or_execute_outer(shell):' | |||||
451 | if d.line_count == 1: |
|
417 | if d.line_count == 1: | |
452 | check_text = d.text |
|
418 | check_text = d.text | |
453 | else: |
|
419 | else: | |
454 | check_text = d.text[:d.cursor_position] |
|
420 | check_text = d.text[: d.cursor_position] | |
455 | status, indent = shell.check_complete(check_text) |
|
421 | status, indent = shell.check_complete(check_text) | |
456 |
|
422 | |||
457 | # if all we have after the cursor is whitespace: reformat current text |
|
423 | # if all we have after the cursor is whitespace: reformat current text | |
458 | # before cursor |
|
424 | # before cursor | |
459 | after_cursor = d.text[d.cursor_position:] |
|
425 | after_cursor = d.text[d.cursor_position :] | |
460 | reformatted = False |
|
426 | reformatted = False | |
461 | if not after_cursor.strip(): |
|
427 | if not after_cursor.strip(): | |
462 | reformat_text_before_cursor(b, d, shell) |
|
428 | reformat_text_before_cursor(b, d, shell) | |
463 | reformatted = True |
|
429 | reformatted = True | |
464 |
if not ( |
|
430 | if not ( | |
465 | d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end() |
|
431 | d.on_last_line | |
466 | ): |
|
432 | or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end() | |
|
433 | ): | |||
467 | if shell.autoindent: |
|
434 | if shell.autoindent: | |
468 |
b.insert_text( |
|
435 | b.insert_text("\n" + indent) | |
469 | else: |
|
436 | else: | |
470 |
b.insert_text( |
|
437 | b.insert_text("\n") | |
471 | return |
|
438 | return | |
472 |
|
439 | |||
473 |
if (status != |
|
440 | if (status != "incomplete") and b.accept_handler: | |
474 | if not reformatted: |
|
441 | if not reformatted: | |
475 | reformat_text_before_cursor(b, d, shell) |
|
442 | reformat_text_before_cursor(b, d, shell) | |
476 | b.validate_and_handle() |
|
443 | b.validate_and_handle() | |
477 | else: |
|
444 | else: | |
478 | if shell.autoindent: |
|
445 | if shell.autoindent: | |
479 |
b.insert_text( |
|
446 | b.insert_text("\n" + indent) | |
480 | else: |
|
447 | else: | |
481 |
b.insert_text( |
|
448 | b.insert_text("\n") | |
|
449 | ||||
|
450 | newline_or_execute.__qualname__ = "newline_or_execute" | |||
|
451 | ||||
482 | return newline_or_execute |
|
452 | return newline_or_execute | |
483 |
|
453 | |||
484 |
|
454 | |||
@@ -501,12 +471,14 b' def next_history_or_next_completion(event):' | |||||
501 |
|
471 | |||
502 |
|
472 | |||
503 | def dismiss_completion(event): |
|
473 | def dismiss_completion(event): | |
|
474 | """Dismiss completion""" | |||
504 | b = event.current_buffer |
|
475 | b = event.current_buffer | |
505 | if b.complete_state: |
|
476 | if b.complete_state: | |
506 | b.cancel_completion() |
|
477 | b.cancel_completion() | |
507 |
|
478 | |||
508 |
|
479 | |||
509 | def reset_buffer(event): |
|
480 | def reset_buffer(event): | |
|
481 | """Reset buffer""" | |||
510 | b = event.current_buffer |
|
482 | b = event.current_buffer | |
511 | if b.complete_state: |
|
483 | if b.complete_state: | |
512 | b.cancel_completion() |
|
484 | b.cancel_completion() | |
@@ -515,16 +487,22 b' def reset_buffer(event):' | |||||
515 |
|
487 | |||
516 |
|
488 | |||
517 | def reset_search_buffer(event): |
|
489 | def reset_search_buffer(event): | |
|
490 | """Reset search buffer""" | |||
518 | if event.current_buffer.document.text: |
|
491 | if event.current_buffer.document.text: | |
519 | event.current_buffer.reset() |
|
492 | event.current_buffer.reset() | |
520 | else: |
|
493 | else: | |
521 | event.app.layout.focus(DEFAULT_BUFFER) |
|
494 | event.app.layout.focus(DEFAULT_BUFFER) | |
522 |
|
495 | |||
|
496 | ||||
523 | def suspend_to_bg(event): |
|
497 | def suspend_to_bg(event): | |
|
498 | """Suspend to background""" | |||
524 | event.app.suspend_to_background() |
|
499 | event.app.suspend_to_background() | |
525 |
|
500 | |||
|
501 | ||||
526 | def quit(event): |
|
502 | def quit(event): | |
527 | """ |
|
503 | """ | |
|
504 | Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise. | |||
|
505 | ||||
528 | On platforms that support SIGQUIT, send SIGQUIT to the current process. |
|
506 | On platforms that support SIGQUIT, send SIGQUIT to the current process. | |
529 | On other platforms, just exit the process with a message. |
|
507 | On other platforms, just exit the process with a message. | |
530 | """ |
|
508 | """ | |
@@ -534,8 +512,11 b' def quit(event):' | |||||
534 | else: |
|
512 | else: | |
535 | sys.exit("Quit") |
|
513 | sys.exit("Quit") | |
536 |
|
514 | |||
|
515 | ||||
537 | def indent_buffer(event): |
|
516 | def indent_buffer(event): | |
538 | event.current_buffer.insert_text(' ' * 4) |
|
517 | """Indent buffer""" | |
|
518 | event.current_buffer.insert_text(" " * 4) | |||
|
519 | ||||
539 |
|
520 | |||
540 | @undoc |
|
521 | @undoc | |
541 | def newline_with_copy_margin(event): |
|
522 | def newline_with_copy_margin(event): | |
@@ -547,9 +528,12 b' def newline_with_copy_margin(event):' | |||||
547 | Preserve margin and cursor position when using |
|
528 | Preserve margin and cursor position when using | |
548 | Control-O to insert a newline in EMACS mode |
|
529 | Control-O to insert a newline in EMACS mode | |
549 | """ |
|
530 | """ | |
550 | warnings.warn("`newline_with_copy_margin(event)` is deprecated since IPython 6.0. " |
|
531 | warnings.warn( | |
551 | "see `newline_autoindent_outer(shell)(event)` for a replacement.", |
|
532 | "`newline_with_copy_margin(event)` is deprecated since IPython 6.0. " | |
552 | DeprecationWarning, stacklevel=2) |
|
533 | "see `newline_autoindent_outer(shell)(event)` for a replacement.", | |
|
534 | DeprecationWarning, | |||
|
535 | stacklevel=2, | |||
|
536 | ) | |||
553 |
|
537 | |||
554 | b = event.current_buffer |
|
538 | b = event.current_buffer | |
555 | cursor_start_pos = b.document.cursor_position_col |
|
539 | cursor_start_pos = b.document.cursor_position_col | |
@@ -560,6 +544,7 b' def newline_with_copy_margin(event):' | |||||
560 | pos_diff = cursor_start_pos - cursor_end_pos |
|
544 | pos_diff = cursor_start_pos - cursor_end_pos | |
561 | b.cursor_right(count=pos_diff) |
|
545 | b.cursor_right(count=pos_diff) | |
562 |
|
546 | |||
|
547 | ||||
563 | def newline_autoindent_outer(inputsplitter) -> Callable[..., None]: |
|
548 | def newline_autoindent_outer(inputsplitter) -> Callable[..., None]: | |
564 | """ |
|
549 | """ | |
565 | Return a function suitable for inserting a indented newline after the cursor. |
|
550 | Return a function suitable for inserting a indented newline after the cursor. | |
@@ -571,28 +556,33 b' def newline_autoindent_outer(inputsplitter) -> Callable[..., None]:' | |||||
571 | """ |
|
556 | """ | |
572 |
|
557 | |||
573 | def newline_autoindent(event): |
|
558 | def newline_autoindent(event): | |
574 |
""" |
|
559 | """Insert a newline after the cursor indented appropriately.""" | |
575 | b = event.current_buffer |
|
560 | b = event.current_buffer | |
576 | d = b.document |
|
561 | d = b.document | |
577 |
|
562 | |||
578 | if b.complete_state: |
|
563 | if b.complete_state: | |
579 | b.cancel_completion() |
|
564 | b.cancel_completion() | |
580 |
text = d.text[:d.cursor_position] + |
|
565 | text = d.text[: d.cursor_position] + "\n" | |
581 | _, indent = inputsplitter.check_complete(text) |
|
566 | _, indent = inputsplitter.check_complete(text) | |
582 |
b.insert_text( |
|
567 | b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False) | |
|
568 | ||||
|
569 | newline_autoindent.__qualname__ = "newline_autoindent" | |||
583 |
|
570 | |||
584 | return newline_autoindent |
|
571 | return newline_autoindent | |
585 |
|
572 | |||
586 |
|
573 | |||
587 | def open_input_in_editor(event): |
|
574 | def open_input_in_editor(event): | |
|
575 | """Open code from input in external editor""" | |||
588 | event.app.current_buffer.open_in_editor() |
|
576 | event.app.current_buffer.open_in_editor() | |
589 |
|
577 | |||
590 |
|
578 | |||
591 |
if sys.platform == |
|
579 | if sys.platform == "win32": | |
592 | from IPython.core.error import TryNext |
|
580 | from IPython.core.error import TryNext | |
593 |
from IPython.lib.clipboard import ( |
|
581 | from IPython.lib.clipboard import ( | |
594 | win32_clipboard_get, |
|
582 | ClipboardEmpty, | |
595 | tkinter_clipboard_get) |
|
583 | win32_clipboard_get, | |
|
584 | tkinter_clipboard_get, | |||
|
585 | ) | |||
596 |
|
586 | |||
597 | @undoc |
|
587 | @undoc | |
598 | def win_paste(event): |
|
588 | def win_paste(event): | |
@@ -606,3 +596,10 b" if sys.platform == 'win32':" | |||||
606 | except ClipboardEmpty: |
|
596 | except ClipboardEmpty: | |
607 | return |
|
597 | return | |
608 | event.current_buffer.insert_text(text.replace("\t", " " * 4)) |
|
598 | event.current_buffer.insert_text(text.replace("\t", " " * 4)) | |
|
599 | ||||
|
600 | else: | |||
|
601 | ||||
|
602 | @undoc | |||
|
603 | def win_paste(event): | |||
|
604 | """Stub used when auto-generating shortcuts for documentation""" | |||
|
605 | pass |
@@ -1,45 +1,98 b'' | |||||
|
1 | from dataclasses import dataclass | |||
|
2 | from inspect import getsource | |||
1 | from pathlib import Path |
|
3 | from pathlib import Path | |
|
4 | from typing import cast, Callable, List, Union | |||
|
5 | from html import escape as html_escape | |||
|
6 | import re | |||
|
7 | ||||
|
8 | from prompt_toolkit.keys import KEY_ALIASES | |||
|
9 | from prompt_toolkit.key_binding import KeyBindingsBase | |||
|
10 | from prompt_toolkit.filters import Filter, Condition | |||
|
11 | from prompt_toolkit.shortcuts import PromptSession | |||
2 |
|
12 | |||
3 | from IPython.terminal.shortcuts import create_ipython_shortcuts |
|
13 | from IPython.terminal.shortcuts import create_ipython_shortcuts | |
4 |
|
14 | |||
5 | def name(c): |
|
|||
6 | s = c.__class__.__name__ |
|
|||
7 | if s == '_Invert': |
|
|||
8 | return '(Not: %s)' % name(c.filter) |
|
|||
9 | if s in log_filters.keys(): |
|
|||
10 | return '(%s: %s)' % (log_filters[s], ', '.join(name(x) for x in c.filters)) |
|
|||
11 | return log_filters[s] if s in log_filters.keys() else s |
|
|||
12 |
|
15 | |||
|
16 | @dataclass | |||
|
17 | class Shortcut: | |||
|
18 | #: a sequence of keys (each element on the list corresponds to pressing one or more keys) | |||
|
19 | keys_sequence: list[str] | |||
|
20 | filter: str | |||
13 |
|
21 | |||
14 | def sentencize(s): |
|
|||
15 | """Extract first sentence |
|
|||
16 | """ |
|
|||
17 | s = s.replace('\n', ' ').strip().split('.') |
|
|||
18 | s = s[0] if len(s) else s |
|
|||
19 | try: |
|
|||
20 | return " ".join(s.split()) |
|
|||
21 | except AttributeError: |
|
|||
22 | return s |
|
|||
23 |
|
22 | |||
|
23 | @dataclass | |||
|
24 | class Handler: | |||
|
25 | description: str | |||
|
26 | identifier: str | |||
24 |
|
27 | |||
25 | def most_common(lst, n=3): |
|
|||
26 | """Most common elements occurring more then `n` times |
|
|||
27 | """ |
|
|||
28 | from collections import Counter |
|
|||
29 |
|
28 | |||
30 | c = Counter(lst) |
|
29 | @dataclass | |
31 | return [k for (k, v) in c.items() if k and v > n] |
|
30 | class Binding: | |
|
31 | handler: Handler | |||
|
32 | shortcut: Shortcut | |||
32 |
|
33 | |||
33 |
|
34 | |||
34 | def multi_filter_str(flt): |
|
35 | class _NestedFilter(Filter): | |
35 | """Yield readable conditional filter |
|
36 | """Protocol reflecting non-public prompt_toolkit's `_AndList` and `_OrList`.""" | |
36 | """ |
|
37 | ||
37 | assert hasattr(flt, 'filters'), 'Conditional filter required' |
|
38 | filters: List[Filter] | |
38 | yield name(flt) |
|
39 | ||
|
40 | ||||
|
41 | class _Invert(Filter): | |||
|
42 | """Protocol reflecting non-public prompt_toolkit's `_Invert`.""" | |||
|
43 | ||||
|
44 | filter: Filter | |||
|
45 | ||||
|
46 | ||||
|
47 | conjunctions_labels = {"_AndList": "and", "_OrList": "or"} | |||
39 |
|
48 | |||
|
49 | ATOMIC_CLASSES = {"Never", "Always", "Condition"} | |||
|
50 | ||||
|
51 | ||||
|
52 | def format_filter( | |||
|
53 | filter_: Union[Filter, _NestedFilter, Condition, _Invert], | |||
|
54 | is_top_level=True, | |||
|
55 | skip=None, | |||
|
56 | ) -> str: | |||
|
57 | """Create easily readable description of the filter.""" | |||
|
58 | s = filter_.__class__.__name__ | |||
|
59 | if s == "Condition": | |||
|
60 | func = cast(Condition, filter_).func | |||
|
61 | name = func.__name__ | |||
|
62 | if name == "<lambda>": | |||
|
63 | source = getsource(func) | |||
|
64 | return source.split("=")[0].strip() | |||
|
65 | return func.__name__ | |||
|
66 | elif s == "_Invert": | |||
|
67 | operand = cast(_Invert, filter_).filter | |||
|
68 | 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)})" | |||
|
71 | elif s in conjunctions_labels: | |||
|
72 | filters = cast(_NestedFilter, filter_).filters | |||
|
73 | conjunction = conjunctions_labels[s] | |||
|
74 | glue = f" {conjunction} " | |||
|
75 | result = glue.join(format_filter(x, is_top_level=False) for x in filters) | |||
|
76 | if len(filters) > 1 and not is_top_level: | |||
|
77 | result = f"({result})" | |||
|
78 | return result | |||
|
79 | elif s in ["Never", "Always"]: | |||
|
80 | return s.lower() | |||
|
81 | else: | |||
|
82 | raise ValueError(f"Unknown filter type: {filter_}") | |||
|
83 | ||||
|
84 | ||||
|
85 | def sentencize(s) -> str: | |||
|
86 | """Extract first sentence""" | |||
|
87 | s = re.split(r"\.\W", s.replace("\n", " ").strip()) | |||
|
88 | s = s[0] if len(s) else "" | |||
|
89 | if not s.endswith("."): | |||
|
90 | s += "." | |||
|
91 | try: | |||
|
92 | return " ".join(s.split()) | |||
|
93 | except AttributeError: | |||
|
94 | return s | |||
40 |
|
95 | |||
41 | log_filters = {'_AndList': 'And', '_OrList': 'Or'} |
|
|||
42 | log_invert = {'_Invert'} |
|
|||
43 |
|
96 | |||
44 | class _DummyTerminal: |
|
97 | class _DummyTerminal: | |
45 | """Used as a buffer to get prompt_toolkit bindings |
|
98 | """Used as a buffer to get prompt_toolkit bindings | |
@@ -50,47 +103,118 b' class _DummyTerminal:' | |||||
50 | editing_mode = "emacs" |
|
103 | editing_mode = "emacs" | |
51 |
|
104 | |||
52 |
|
105 | |||
53 | ipy_bindings = create_ipython_shortcuts(_DummyTerminal()).bindings |
|
106 | def create_identifier(handler: Callable): | |
54 |
|
107 | parts = handler.__module__.split(".") | ||
55 | dummy_docs = [] # ignore bindings without proper documentation |
|
108 | name = handler.__name__ | |
56 |
|
109 | package = parts[0] | ||
57 | common_docs = most_common([kb.handler.__doc__ for kb in ipy_bindings]) |
|
110 | if len(parts) > 1: | |
58 | if common_docs: |
|
111 | final_module = parts[-1] | |
59 | dummy_docs.extend(common_docs) |
|
112 | return f"{package}:{final_module}.{name}" | |
|
113 | else: | |||
|
114 | return f"{package}:{name}" | |||
|
115 | ||||
|
116 | ||||
|
117 | def bindings_from_prompt_toolkit(prompt_bindings: KeyBindingsBase) -> List[Binding]: | |||
|
118 | """Collect bindings to a simple format that does not depend on prompt-toolkit internals""" | |||
|
119 | bindings: List[Binding] = [] | |||
|
120 | ||||
|
121 | for kb in prompt_bindings.bindings: | |||
|
122 | bindings.append( | |||
|
123 | Binding( | |||
|
124 | handler=Handler( | |||
|
125 | description=kb.handler.__doc__ or "", | |||
|
126 | identifier=create_identifier(kb.handler), | |||
|
127 | ), | |||
|
128 | shortcut=Shortcut( | |||
|
129 | keys_sequence=[ | |||
|
130 | str(k.value) if hasattr(k, "value") else k for k in kb.keys | |||
|
131 | ], | |||
|
132 | filter=format_filter(kb.filter, skip={"has_focus_filter"}), | |||
|
133 | ), | |||
|
134 | ) | |||
|
135 | ) | |||
|
136 | return bindings | |||
|
137 | ||||
|
138 | ||||
|
139 | INDISTINGUISHABLE_KEYS = {**KEY_ALIASES, **{v: k for k, v in KEY_ALIASES.items()}} | |||
|
140 | ||||
|
141 | ||||
|
142 | def format_prompt_keys(keys: str, add_alternatives=True) -> str: | |||
|
143 | """Format prompt toolkit key with modifier into an RST representation.""" | |||
|
144 | ||||
|
145 | def to_rst(key): | |||
|
146 | escaped = key.replace("\\", "\\\\") | |||
|
147 | return f":kbd:`{escaped}`" | |||
|
148 | ||||
|
149 | keys_to_press: list[str] | |||
|
150 | ||||
|
151 | prefixes = { | |||
|
152 | "c-s-": [to_rst("ctrl"), to_rst("shift")], | |||
|
153 | "s-c-": [to_rst("ctrl"), to_rst("shift")], | |||
|
154 | "c-": [to_rst("ctrl")], | |||
|
155 | "s-": [to_rst("shift")], | |||
|
156 | } | |||
|
157 | ||||
|
158 | for prefix, modifiers in prefixes.items(): | |||
|
159 | if keys.startswith(prefix): | |||
|
160 | remainder = keys[len(prefix) :] | |||
|
161 | keys_to_press = [*modifiers, to_rst(remainder)] | |||
|
162 | break | |||
|
163 | else: | |||
|
164 | keys_to_press = [to_rst(keys)] | |||
60 |
|
165 | |||
61 | dummy_docs = list(set(dummy_docs)) |
|
166 | result = " + ".join(keys_to_press) | |
62 |
|
167 | |||
63 | single_filter = {} |
|
168 | if keys in INDISTINGUISHABLE_KEYS and add_alternatives: | |
64 | multi_filter = {} |
|
169 | alternative = INDISTINGUISHABLE_KEYS[keys] | |
65 | for kb in ipy_bindings: |
|
|||
66 | doc = kb.handler.__doc__ |
|
|||
67 | if not doc or doc in dummy_docs: |
|
|||
68 | continue |
|
|||
69 |
|
170 | |||
70 | shortcut = ' '.join([k if isinstance(k, str) else k.name for k in kb.keys]) |
|
171 | result = ( | |
71 | shortcut += shortcut.endswith('\\') and '\\' or '' |
|
172 | result | |
72 | if hasattr(kb.filter, 'filters'): |
|
173 | + " (or " | |
73 | flt = ' '.join(multi_filter_str(kb.filter)) |
|
174 | + format_prompt_keys(alternative, add_alternatives=False) | |
74 | multi_filter[(shortcut, flt)] = sentencize(doc) |
|
175 | + ")" | |
75 | else: |
|
176 | ) | |
76 | single_filter[(shortcut, name(kb.filter))] = sentencize(doc) |
|
|||
77 |
|
177 | |||
|
178 | return result | |||
78 |
|
179 | |||
79 | if __name__ == '__main__': |
|
180 | if __name__ == '__main__': | |
80 | here = Path(__file__).parent |
|
181 | here = Path(__file__).parent | |
81 | dest = here / "source" / "config" / "shortcuts" |
|
182 | dest = here / "source" / "config" / "shortcuts" | |
82 |
|
183 | |||
83 | def sort_key(item): |
|
184 | ipy_bindings = create_ipython_shortcuts(_DummyTerminal(), for_all_platforms=True) | |
84 | k, v = item |
|
185 | ||
85 | shortcut, flt = k |
|
186 | session = PromptSession(key_bindings=ipy_bindings) | |
86 | return (str(shortcut), str(flt)) |
|
187 | prompt_bindings = session.app.key_bindings | |
87 |
|
188 | |||
88 | for filters, output_filename in [ |
|
189 | assert prompt_bindings | |
89 | (single_filter, "single_filtered"), |
|
190 | # Ensure that we collected the default shortcuts | |
90 | (multi_filter, "multi_filtered"), |
|
191 | assert len(prompt_bindings.bindings) > len(ipy_bindings.bindings) | |
91 | ]: |
|
192 | ||
92 | with (dest / "{}.csv".format(output_filename)).open( |
|
193 | bindings = bindings_from_prompt_toolkit(prompt_bindings) | |
93 | "w", encoding="utf-8" |
|
194 | ||
94 | ) as csv: |
|
195 | def sort_key(binding: Binding): | |
95 | for (shortcut, flt), v in sorted(filters.items(), key=sort_key): |
|
196 | return binding.handler.identifier, binding.shortcut.filter | |
96 | csv.write(":kbd:`{}`\t{}\t{}\n".format(shortcut, flt, v)) |
|
197 | ||
|
198 | filters = [] | |||
|
199 | with (dest / "table.tsv").open("w", encoding="utf-8") as csv: | |||
|
200 | for binding in sorted(bindings, key=sort_key): | |||
|
201 | sequence = ", ".join( | |||
|
202 | [format_prompt_keys(keys) for keys in binding.shortcut.keys_sequence] | |||
|
203 | ) | |||
|
204 | if binding.shortcut.filter == "always": | |||
|
205 | condition_label = "-" | |||
|
206 | else: | |||
|
207 | # we cannot fit all the columns as the filters got too complex over time | |||
|
208 | condition_label = "ⓘ" | |||
|
209 | ||||
|
210 | csv.write( | |||
|
211 | "\t".join( | |||
|
212 | [ | |||
|
213 | sequence, | |||
|
214 | sentencize(binding.handler.description) | |||
|
215 | + f" :raw-html:`<br>` `{binding.handler.identifier}`", | |||
|
216 | f':raw-html:`<span title="{html_escape(binding.shortcut.filter)}" style="cursor: help">{condition_label}</span>`', | |||
|
217 | ] | |||
|
218 | ) | |||
|
219 | + "\n" | |||
|
220 | ) |
@@ -211,7 +211,6 b" default_role = 'literal'" | |||||
211 | # given in html_static_path. |
|
211 | # given in html_static_path. | |
212 | # html_style = 'default.css' |
|
212 | # html_style = 'default.css' | |
213 |
|
213 | |||
214 |
|
||||
215 | # The name for this set of Sphinx documents. If None, it defaults to |
|
214 | # The name for this set of Sphinx documents. If None, it defaults to | |
216 | # "<project> v<release> documentation". |
|
215 | # "<project> v<release> documentation". | |
217 | #html_title = None |
|
216 | #html_title = None | |
@@ -327,6 +326,10 b' texinfo_documents = [' | |||||
327 | modindex_common_prefix = ['IPython.'] |
|
326 | modindex_common_prefix = ['IPython.'] | |
328 |
|
327 | |||
329 |
|
328 | |||
|
329 | def setup(app): | |||
|
330 | app.add_css_file("theme_overrides.css") | |||
|
331 | ||||
|
332 | ||||
330 | # Cleanup |
|
333 | # Cleanup | |
331 | # ------- |
|
334 | # ------- | |
332 | # delete release info to avoid pickling errors from sphinx |
|
335 | # delete release info to avoid pickling errors from sphinx |
@@ -4,28 +4,23 b' IPython shortcuts' | |||||
4 |
|
4 | |||
5 | Available shortcuts in an IPython terminal. |
|
5 | Available shortcuts in an IPython terminal. | |
6 |
|
6 | |||
7 | .. warning:: |
|
7 | .. note:: | |
8 |
|
8 | |||
9 |
This list is automatically generated |
|
9 | This list is automatically generated. Key bindings defined in ``prompt_toolkit`` may differ | |
10 | shortcuts. In particular, it may depend on the version of ``prompt_toolkit`` |
|
10 | between installations depending on the ``prompt_toolkit`` version. | |
11 | installed during the generation of this page. |
|
|||
12 |
|
11 | |||
13 |
|
|
12 | ||
14 | Single Filtered shortcuts |
|
13 | * Comma-separated keys, e.g. :kbd:`Esc`, :kbd:`f`, indicate a sequence which can be activated by pressing the listed keys in succession. | |
15 | ========================= |
|
14 | * Plus-separated keys, e.g. :kbd:`Esc` + :kbd:`f` indicate a combination which requires pressing all keys simultaneously. | |
16 |
|
15 | * Hover over the ⓘ icon in the filter column to see when the shortcut is active.g | ||
17 | .. csv-table:: |
|
|||
18 | :header: Shortcut,Filter,Description |
|
|||
19 | :widths: 30, 30, 100 |
|
|||
20 | :delim: tab |
|
|||
21 | :file: single_filtered.csv |
|
|||
22 |
|
16 | |||
|
17 | .. role:: raw-html(raw) | |||
|
18 | :format: html | |||
23 |
|
19 | |||
24 | Multi Filtered shortcuts |
|
|||
25 | ======================== |
|
|||
26 |
|
20 | |||
27 | .. csv-table:: |
|
21 | .. csv-table:: | |
28 |
:header: Shortcut, |
|
22 | :header: Shortcut,Description and identifier,Filter | |
29 | :widths: 30, 30, 100 |
|
|||
30 | :delim: tab |
|
23 | :delim: tab | |
31 | :file: multi_filtered.csv |
|
24 | :class: shortcuts | |
|
25 | :file: table.tsv | |||
|
26 | :widths: 20 75 5 |
General Comments 0
You need to be logged in to leave comments.
Login now