Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/IPython/terminal/shortcuts/__init__.py: 33%
180 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +0000
1"""
2Module to define and register Terminal IPython shortcuts with
3:mod:`prompt_toolkit`
4"""
6# Copyright (c) IPython Development Team.
7# Distributed under the terms of the Modified BSD License.
9import os
10import signal
11import sys
12import warnings
13from dataclasses import dataclass
14from typing import Callable, Any, Optional, List
16from prompt_toolkit.application.current import get_app
17from prompt_toolkit.key_binding import KeyBindings
18from prompt_toolkit.key_binding.key_processor import KeyPressEvent
19from prompt_toolkit.key_binding.bindings import named_commands as nc
20from prompt_toolkit.key_binding.bindings.completion import (
21 display_completions_like_readline,
22)
23from prompt_toolkit.key_binding.vi_state import InputMode, ViState
24from prompt_toolkit.filters import Condition
26from IPython.core.getipython import get_ipython
27from IPython.terminal.shortcuts import auto_match as match
28from IPython.terminal.shortcuts import auto_suggest
29from IPython.terminal.shortcuts.filters import filter_from_string
30from IPython.utils.decorators import undoc
32from prompt_toolkit.enums import DEFAULT_BUFFER
34__all__ = ["create_ipython_shortcuts"]
37@dataclass
38class BaseBinding:
39 command: Callable[[KeyPressEvent], Any]
40 keys: List[str]
43@dataclass
44class RuntimeBinding(BaseBinding):
45 filter: Condition
48@dataclass
49class Binding(BaseBinding):
50 # while filter could be created by referencing variables directly (rather
51 # than created from strings), by using strings we ensure that users will
52 # be able to create filters in configuration (e.g. JSON) files too, which
53 # also benefits the documentation by enforcing human-readable filter names.
54 condition: Optional[str] = None
56 def __post_init__(self):
57 if self.condition:
58 self.filter = filter_from_string(self.condition)
59 else:
60 self.filter = None
63def create_identifier(handler: Callable):
64 parts = handler.__module__.split(".")
65 name = handler.__name__
66 package = parts[0]
67 if len(parts) > 1:
68 final_module = parts[-1]
69 return f"{package}:{final_module}.{name}"
70 else:
71 return f"{package}:{name}"
74AUTO_MATCH_BINDINGS = [
75 *[
76 Binding(
77 cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
78 )
79 for key, cmd in match.auto_match_parens.items()
80 ],
81 *[
82 # raw string
83 Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
84 for key, cmd in match.auto_match_parens_raw_string.items()
85 ],
86 Binding(
87 match.double_quote,
88 ['"'],
89 "focused_insert"
90 " & auto_match"
91 " & not_inside_unclosed_string"
92 " & preceded_by_paired_double_quotes"
93 " & followed_by_closing_paren_or_end",
94 ),
95 Binding(
96 match.single_quote,
97 ["'"],
98 "focused_insert"
99 " & auto_match"
100 " & not_inside_unclosed_string"
101 " & preceded_by_paired_single_quotes"
102 " & followed_by_closing_paren_or_end",
103 ),
104 Binding(
105 match.docstring_double_quotes,
106 ['"'],
107 "focused_insert"
108 " & auto_match"
109 " & not_inside_unclosed_string"
110 " & preceded_by_two_double_quotes",
111 ),
112 Binding(
113 match.docstring_single_quotes,
114 ["'"],
115 "focused_insert"
116 " & auto_match"
117 " & not_inside_unclosed_string"
118 " & preceded_by_two_single_quotes",
119 ),
120 Binding(
121 match.skip_over,
122 [")"],
123 "focused_insert & auto_match & followed_by_closing_round_paren",
124 ),
125 Binding(
126 match.skip_over,
127 ["]"],
128 "focused_insert & auto_match & followed_by_closing_bracket",
129 ),
130 Binding(
131 match.skip_over,
132 ["}"],
133 "focused_insert & auto_match & followed_by_closing_brace",
134 ),
135 Binding(
136 match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
137 ),
138 Binding(
139 match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
140 ),
141 Binding(
142 match.delete_pair,
143 ["backspace"],
144 "focused_insert"
145 " & preceded_by_opening_round_paren"
146 " & auto_match"
147 " & followed_by_closing_round_paren",
148 ),
149 Binding(
150 match.delete_pair,
151 ["backspace"],
152 "focused_insert"
153 " & preceded_by_opening_bracket"
154 " & auto_match"
155 " & followed_by_closing_bracket",
156 ),
157 Binding(
158 match.delete_pair,
159 ["backspace"],
160 "focused_insert"
161 " & preceded_by_opening_brace"
162 " & auto_match"
163 " & followed_by_closing_brace",
164 ),
165 Binding(
166 match.delete_pair,
167 ["backspace"],
168 "focused_insert"
169 " & preceded_by_double_quote"
170 " & auto_match"
171 " & followed_by_double_quote",
172 ),
173 Binding(
174 match.delete_pair,
175 ["backspace"],
176 "focused_insert"
177 " & preceded_by_single_quote"
178 " & auto_match"
179 " & followed_by_single_quote",
180 ),
181]
183AUTO_SUGGEST_BINDINGS = [
184 # there are two reasons for re-defining bindings defined upstream:
185 # 1) prompt-toolkit does not execute autosuggestion bindings in vi mode,
186 # 2) prompt-toolkit checks if we are at the end of text, not end of line
187 # hence it does not work in multi-line mode of navigable provider
188 Binding(
189 auto_suggest.accept_or_jump_to_end,
190 ["end"],
191 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
192 ),
193 Binding(
194 auto_suggest.accept_or_jump_to_end,
195 ["c-e"],
196 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
197 ),
198 Binding(
199 auto_suggest.accept,
200 ["c-f"],
201 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
202 ),
203 Binding(
204 auto_suggest.accept,
205 ["right"],
206 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
207 ),
208 Binding(
209 auto_suggest.accept_word,
210 ["escape", "f"],
211 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
212 ),
213 Binding(
214 auto_suggest.accept_token,
215 ["c-right"],
216 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
217 ),
218 Binding(
219 auto_suggest.discard,
220 ["escape"],
221 # note this one is using `emacs_insert_mode`, not `emacs_like_insert_mode`
222 # as in `vi_insert_mode` we do not want `escape` to be shadowed (ever).
223 "has_suggestion & default_buffer_focused & emacs_insert_mode",
224 ),
225 Binding(
226 auto_suggest.discard,
227 ["delete"],
228 "has_suggestion & default_buffer_focused & emacs_insert_mode",
229 ),
230 Binding(
231 auto_suggest.swap_autosuggestion_up,
232 ["c-up"],
233 "navigable_suggestions"
234 " & ~has_line_above"
235 " & has_suggestion"
236 " & default_buffer_focused",
237 ),
238 Binding(
239 auto_suggest.swap_autosuggestion_down,
240 ["c-down"],
241 "navigable_suggestions"
242 " & ~has_line_below"
243 " & has_suggestion"
244 " & default_buffer_focused",
245 ),
246 Binding(
247 auto_suggest.up_and_update_hint,
248 ["c-up"],
249 "has_line_above & navigable_suggestions & default_buffer_focused",
250 ),
251 Binding(
252 auto_suggest.down_and_update_hint,
253 ["c-down"],
254 "has_line_below & navigable_suggestions & default_buffer_focused",
255 ),
256 Binding(
257 auto_suggest.accept_character,
258 ["escape", "right"],
259 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
260 ),
261 Binding(
262 auto_suggest.accept_and_move_cursor_left,
263 ["c-left"],
264 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
265 ),
266 Binding(
267 auto_suggest.accept_and_keep_cursor,
268 ["escape", "down"],
269 "has_suggestion & default_buffer_focused & emacs_insert_mode",
270 ),
271 Binding(
272 auto_suggest.backspace_and_resume_hint,
273 ["backspace"],
274 # no `has_suggestion` here to allow resuming if no suggestion
275 "default_buffer_focused & emacs_like_insert_mode",
276 ),
277 Binding(
278 auto_suggest.resume_hinting,
279 ["right"],
280 "is_cursor_at_the_end_of_line"
281 " & default_buffer_focused"
282 " & emacs_like_insert_mode"
283 " & pass_through",
284 ),
285]
288SIMPLE_CONTROL_BINDINGS = [
289 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
290 for key, cmd in {
291 "c-a": nc.beginning_of_line,
292 "c-b": nc.backward_char,
293 "c-k": nc.kill_line,
294 "c-w": nc.backward_kill_word,
295 "c-y": nc.yank,
296 "c-_": nc.undo,
297 }.items()
298]
301ALT_AND_COMOBO_CONTROL_BINDINGS = [
302 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
303 for keys, cmd in {
304 # Control Combos
305 ("c-x", "c-e"): nc.edit_and_execute,
306 ("c-x", "e"): nc.edit_and_execute,
307 # Alt
308 ("escape", "b"): nc.backward_word,
309 ("escape", "c"): nc.capitalize_word,
310 ("escape", "d"): nc.kill_word,
311 ("escape", "h"): nc.backward_kill_word,
312 ("escape", "l"): nc.downcase_word,
313 ("escape", "u"): nc.uppercase_word,
314 ("escape", "y"): nc.yank_pop,
315 ("escape", "."): nc.yank_last_arg,
316 }.items()
317]
320def add_binding(bindings: KeyBindings, binding: Binding):
321 bindings.add(
322 *binding.keys,
323 **({"filter": binding.filter} if binding.filter is not None else {}),
324 )(binding.command)
327def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
328 """Set up the prompt_toolkit keyboard shortcuts for IPython.
330 Parameters
331 ----------
332 shell: InteractiveShell
333 The current IPython shell Instance
334 skip: List[Binding]
335 Bindings to skip.
337 Returns
338 -------
339 KeyBindings
340 the keybinding instance for prompt toolkit.
342 """
343 kb = KeyBindings()
344 skip = skip or []
345 for binding in KEY_BINDINGS:
346 skip_this_one = False
347 for to_skip in skip:
348 if (
349 to_skip.command == binding.command
350 and to_skip.filter == binding.filter
351 and to_skip.keys == binding.keys
352 ):
353 skip_this_one = True
354 break
355 if skip_this_one:
356 continue
357 add_binding(kb, binding)
359 def get_input_mode(self):
360 app = get_app()
361 app.ttimeoutlen = shell.ttimeoutlen
362 app.timeoutlen = shell.timeoutlen
364 return self._input_mode
366 def set_input_mode(self, mode):
367 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
368 cursor = "\x1b[{} q".format(shape)
370 sys.stdout.write(cursor)
371 sys.stdout.flush()
373 self._input_mode = mode
375 if shell.editing_mode == "vi" and shell.modal_cursor:
376 ViState._input_mode = InputMode.INSERT # type: ignore
377 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
379 return kb
382def reformat_and_execute(event):
383 """Reformat code and execute it"""
384 shell = get_ipython()
385 reformat_text_before_cursor(
386 event.current_buffer, event.current_buffer.document, shell
387 )
388 event.current_buffer.validate_and_handle()
391def reformat_text_before_cursor(buffer, document, shell):
392 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
393 try:
394 formatted_text = shell.reformat_handler(text)
395 buffer.insert_text(formatted_text)
396 except Exception as e:
397 buffer.insert_text(text)
400def handle_return_or_newline_or_execute(event):
401 shell = get_ipython()
402 if getattr(shell, "handle_return", None):
403 return shell.handle_return(shell)(event)
404 else:
405 return newline_or_execute_outer(shell)(event)
408def newline_or_execute_outer(shell):
409 def newline_or_execute(event):
410 """When the user presses return, insert a newline or execute the code."""
411 b = event.current_buffer
412 d = b.document
414 if b.complete_state:
415 cc = b.complete_state.current_completion
416 if cc:
417 b.apply_completion(cc)
418 else:
419 b.cancel_completion()
420 return
422 # If there's only one line, treat it as if the cursor is at the end.
423 # See https://github.com/ipython/ipython/issues/10425
424 if d.line_count == 1:
425 check_text = d.text
426 else:
427 check_text = d.text[: d.cursor_position]
428 status, indent = shell.check_complete(check_text)
430 # if all we have after the cursor is whitespace: reformat current text
431 # before cursor
432 after_cursor = d.text[d.cursor_position :]
433 reformatted = False
434 if not after_cursor.strip():
435 reformat_text_before_cursor(b, d, shell)
436 reformatted = True
437 if not (
438 d.on_last_line
439 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
440 ):
441 if shell.autoindent:
442 b.insert_text("\n" + indent)
443 else:
444 b.insert_text("\n")
445 return
447 if (status != "incomplete") and b.accept_handler:
448 if not reformatted:
449 reformat_text_before_cursor(b, d, shell)
450 b.validate_and_handle()
451 else:
452 if shell.autoindent:
453 b.insert_text("\n" + indent)
454 else:
455 b.insert_text("\n")
457 return newline_or_execute
460def previous_history_or_previous_completion(event):
461 """
462 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
464 If completer is open this still select previous completion.
465 """
466 event.current_buffer.auto_up()
469def next_history_or_next_completion(event):
470 """
471 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
473 If completer is open this still select next completion.
474 """
475 event.current_buffer.auto_down()
478def dismiss_completion(event):
479 """Dismiss completion"""
480 b = event.current_buffer
481 if b.complete_state:
482 b.cancel_completion()
485def reset_buffer(event):
486 """Reset buffer"""
487 b = event.current_buffer
488 if b.complete_state:
489 b.cancel_completion()
490 else:
491 b.reset()
494def reset_search_buffer(event):
495 """Reset search buffer"""
496 if event.current_buffer.document.text:
497 event.current_buffer.reset()
498 else:
499 event.app.layout.focus(DEFAULT_BUFFER)
502def suspend_to_bg(event):
503 """Suspend to background"""
504 event.app.suspend_to_background()
507def quit(event):
508 """
509 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
511 On platforms that support SIGQUIT, send SIGQUIT to the current process.
512 On other platforms, just exit the process with a message.
513 """
514 sigquit = getattr(signal, "SIGQUIT", None)
515 if sigquit is not None:
516 os.kill(0, signal.SIGQUIT)
517 else:
518 sys.exit("Quit")
521def indent_buffer(event):
522 """Indent buffer"""
523 event.current_buffer.insert_text(" " * 4)
526def newline_autoindent(event):
527 """Insert a newline after the cursor indented appropriately.
529 Fancier version of former ``newline_with_copy_margin`` which should
530 compute the correct indentation of the inserted line. That is to say, indent
531 by 4 extra space after a function definition, class definition, context
532 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
533 """
534 shell = get_ipython()
535 inputsplitter = shell.input_transformer_manager
536 b = event.current_buffer
537 d = b.document
539 if b.complete_state:
540 b.cancel_completion()
541 text = d.text[: d.cursor_position] + "\n"
542 _, indent = inputsplitter.check_complete(text)
543 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
546def open_input_in_editor(event):
547 """Open code from input in external editor"""
548 event.app.current_buffer.open_in_editor()
551if sys.platform == "win32":
552 from IPython.core.error import TryNext
553 from IPython.lib.clipboard import (
554 ClipboardEmpty,
555 tkinter_clipboard_get,
556 win32_clipboard_get,
557 )
559 @undoc
560 def win_paste(event):
561 try:
562 text = win32_clipboard_get()
563 except TryNext:
564 try:
565 text = tkinter_clipboard_get()
566 except (TryNext, ClipboardEmpty):
567 return
568 except ClipboardEmpty:
569 return
570 event.current_buffer.insert_text(text.replace("\t", " " * 4))
572else:
574 @undoc
575 def win_paste(event):
576 """Stub used on other platforms"""
577 pass
580KEY_BINDINGS = [
581 Binding(
582 handle_return_or_newline_or_execute,
583 ["enter"],
584 "default_buffer_focused & ~has_selection & insert_mode",
585 ),
586 Binding(
587 reformat_and_execute,
588 ["escape", "enter"],
589 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
590 ),
591 Binding(quit, ["c-\\"]),
592 Binding(
593 previous_history_or_previous_completion,
594 ["c-p"],
595 "vi_insert_mode & default_buffer_focused",
596 ),
597 Binding(
598 next_history_or_next_completion,
599 ["c-n"],
600 "vi_insert_mode & default_buffer_focused",
601 ),
602 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
603 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
604 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
605 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
606 Binding(
607 indent_buffer,
608 ["tab"], # Ctrl+I == Tab
609 "default_buffer_focused"
610 " & ~has_selection"
611 " & insert_mode"
612 " & cursor_in_leading_ws",
613 ),
614 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
615 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
616 *AUTO_MATCH_BINDINGS,
617 *AUTO_SUGGEST_BINDINGS,
618 Binding(
619 display_completions_like_readline,
620 ["c-i"],
621 "readline_like_completions"
622 " & default_buffer_focused"
623 " & ~has_selection"
624 " & insert_mode"
625 " & ~cursor_in_leading_ws",
626 ),
627 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
628 *SIMPLE_CONTROL_BINDINGS,
629 *ALT_AND_COMOBO_CONTROL_BINDINGS,
630]