##// END OF EJS Templates
Restore shortcuts in documentation, define identifiers
krassowski -
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 (has_focus, has_selection, Condition,
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, 'handle_return', None):
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('enter', filter=(has_focus(DEFAULT_BUFFER)
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('c-p', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER))
94 kb.add("c-p", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))(
84 )(previous_history_or_previous_completion)
95 previous_history_or_previous_completion
96 )
85
97
86 kb.add('c-n', filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER))
98 kb.add("c-n", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))(
87 )(next_history_or_next_completion)
99 next_history_or_next_completion
100 )
88
101
89 kb.add('c-g', filter=(has_focus(DEFAULT_BUFFER) & has_completions)
102 kb.add("c-g", filter=(has_focus(DEFAULT_BUFFER) & has_completions))(
90 )(dismiss_completion)
103 dismiss_completion
104 )
91
105
92 kb.add('c-c', filter=has_focus(DEFAULT_BUFFER))(reset_buffer)
106 kb.add("c-c", filter=has_focus(DEFAULT_BUFFER))(reset_buffer)
93
107
94 kb.add('c-c', filter=has_focus(SEARCH_BUFFER))(reset_search_buffer)
108 kb.add("c-c", filter=has_focus(SEARCH_BUFFER))(reset_search_buffer)
95
109
96 supports_suspend = Condition(lambda: hasattr(signal, 'SIGTSTP'))
110 supports_suspend = Condition(lambda: hasattr(signal, "SIGTSTP"))
97 kb.add('c-z', filter=supports_suspend)(suspend_to_bg)
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('f2', filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor)
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 @kb.add(
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 @kb.add(
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 @kb.add(
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 @kb.add(
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 "[", filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$")
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 matches = re.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 @kb.add(
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 @kb.add(
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 @kb.add(
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 @kb.add(
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 @kb.add(
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 @kb.add("c-e", filter=focused_insert_vi & ebivim)
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 @kb.add("c-f", filter=focused_insert_vi)
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 (d.on_last_line or
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('\n' + indent)
435 b.insert_text("\n" + indent)
469 else:
436 else:
470 b.insert_text('\n')
437 b.insert_text("\n")
471 return
438 return
472
439
473 if (status != 'incomplete') and b.accept_handler:
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('\n' + indent)
446 b.insert_text("\n" + indent)
480 else:
447 else:
481 b.insert_text('\n')
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 """insert a newline after the cursor indented appropriately."""
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] + '\n'
565 text = d.text[: d.cursor_position] + "\n"
581 _, indent = inputsplitter.check_complete(text)
566 _, indent = inputsplitter.check_complete(text)
582 b.insert_text('\n' + (' ' * (indent or 0)), move_cursor=False)
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 == 'win32':
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 (ClipboardEmpty,
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, and may not hold all available
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,Filter,Description
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