##// END OF EJS Templates
Only enable resume hinting with arrow if at the end of the line
krassowski -
Show More
@@ -1,627 +1,629 b''
1 1 """
2 2 Module to define and register Terminal IPython shortcuts with
3 3 :mod:`prompt_toolkit`
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 import os
10 10 import signal
11 11 import sys
12 12 import warnings
13 13 from dataclasses import dataclass
14 14 from typing import Callable, Any, Optional, List
15 15
16 16 from prompt_toolkit.application.current import get_app
17 17 from prompt_toolkit.key_binding import KeyBindings
18 18 from prompt_toolkit.key_binding.key_processor import KeyPressEvent
19 19 from prompt_toolkit.key_binding.bindings import named_commands as nc
20 20 from prompt_toolkit.key_binding.bindings.completion import (
21 21 display_completions_like_readline,
22 22 )
23 23 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
24 24 from prompt_toolkit.filters import Condition
25 25
26 26 from IPython.core.getipython import get_ipython
27 27 from IPython.terminal.shortcuts import auto_match as match
28 28 from IPython.terminal.shortcuts import auto_suggest
29 29 from IPython.terminal.shortcuts.filters import filter_from_string
30 30 from IPython.utils.decorators import undoc
31 31
32 32 from prompt_toolkit.enums import DEFAULT_BUFFER
33 33
34 34 __all__ = ["create_ipython_shortcuts"]
35 35
36 36
37 37 @dataclass
38 38 class BaseBinding:
39 39 command: Callable[[KeyPressEvent], Any]
40 40 keys: List[str]
41 41
42 42
43 43 @dataclass
44 44 class RuntimeBinding(BaseBinding):
45 45 filter: Condition
46 46
47 47
48 48 @dataclass
49 49 class Binding(BaseBinding):
50 50 # while filter could be created by referencing variables directly (rather
51 51 # than created from strings), by using strings we ensure that users will
52 52 # be able to create filters in configuration (e.g. JSON) files too, which
53 53 # also benefits the documentation by enforcing human-readable filter names.
54 54 condition: Optional[str] = None
55 55
56 56 def __post_init__(self):
57 57 if self.condition:
58 58 self.filter = filter_from_string(self.condition)
59 59 else:
60 60 self.filter = None
61 61
62 62
63 63 def create_identifier(handler: Callable):
64 64 parts = handler.__module__.split(".")
65 65 name = handler.__name__
66 66 package = parts[0]
67 67 if len(parts) > 1:
68 68 final_module = parts[-1]
69 69 return f"{package}:{final_module}.{name}"
70 70 else:
71 71 return f"{package}:{name}"
72 72
73 73
74 74 AUTO_MATCH_BINDINGS = [
75 75 *[
76 76 Binding(
77 77 cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
78 78 )
79 79 for key, cmd in match.auto_match_parens.items()
80 80 ],
81 81 *[
82 82 # raw string
83 83 Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
84 84 for key, cmd in match.auto_match_parens_raw_string.items()
85 85 ],
86 86 Binding(
87 87 match.double_quote,
88 88 ['"'],
89 89 "focused_insert"
90 90 " & auto_match"
91 91 " & not_inside_unclosed_string"
92 92 " & preceded_by_paired_double_quotes"
93 93 " & followed_by_closing_paren_or_end",
94 94 ),
95 95 Binding(
96 96 match.single_quote,
97 97 ["'"],
98 98 "focused_insert"
99 99 " & auto_match"
100 100 " & not_inside_unclosed_string"
101 101 " & preceded_by_paired_single_quotes"
102 102 " & followed_by_closing_paren_or_end",
103 103 ),
104 104 Binding(
105 105 match.docstring_double_quotes,
106 106 ['"'],
107 107 "focused_insert"
108 108 " & auto_match"
109 109 " & not_inside_unclosed_string"
110 110 " & preceded_by_two_double_quotes",
111 111 ),
112 112 Binding(
113 113 match.docstring_single_quotes,
114 114 ["'"],
115 115 "focused_insert"
116 116 " & auto_match"
117 117 " & not_inside_unclosed_string"
118 118 " & preceded_by_two_single_quotes",
119 119 ),
120 120 Binding(
121 121 match.skip_over,
122 122 [")"],
123 123 "focused_insert & auto_match & followed_by_closing_round_paren",
124 124 ),
125 125 Binding(
126 126 match.skip_over,
127 127 ["]"],
128 128 "focused_insert & auto_match & followed_by_closing_bracket",
129 129 ),
130 130 Binding(
131 131 match.skip_over,
132 132 ["}"],
133 133 "focused_insert & auto_match & followed_by_closing_brace",
134 134 ),
135 135 Binding(
136 136 match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
137 137 ),
138 138 Binding(
139 139 match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
140 140 ),
141 141 Binding(
142 142 match.delete_pair,
143 143 ["backspace"],
144 144 "focused_insert"
145 145 " & preceded_by_opening_round_paren"
146 146 " & auto_match"
147 147 " & followed_by_closing_round_paren",
148 148 ),
149 149 Binding(
150 150 match.delete_pair,
151 151 ["backspace"],
152 152 "focused_insert"
153 153 " & preceded_by_opening_bracket"
154 154 " & auto_match"
155 155 " & followed_by_closing_bracket",
156 156 ),
157 157 Binding(
158 158 match.delete_pair,
159 159 ["backspace"],
160 160 "focused_insert"
161 161 " & preceded_by_opening_brace"
162 162 " & auto_match"
163 163 " & followed_by_closing_brace",
164 164 ),
165 165 Binding(
166 166 match.delete_pair,
167 167 ["backspace"],
168 168 "focused_insert"
169 169 " & preceded_by_double_quote"
170 170 " & auto_match"
171 171 " & followed_by_double_quote",
172 172 ),
173 173 Binding(
174 174 match.delete_pair,
175 175 ["backspace"],
176 176 "focused_insert"
177 177 " & preceded_by_single_quote"
178 178 " & auto_match"
179 179 " & followed_by_single_quote",
180 180 ),
181 181 ]
182 182
183 183 AUTO_SUGGEST_BINDINGS = [
184 184 # there are two reasons for re-defining bindings defined upstream:
185 185 # 1) prompt-toolkit does not execute autosuggestion bindings in vi mode,
186 186 # 2) prompt-toolkit checks if we are at the end of text, not end of line
187 187 # hence it does not work in multi-line mode of navigable provider
188 188 Binding(
189 189 auto_suggest.accept_or_jump_to_end,
190 190 ["end"],
191 191 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
192 192 ),
193 193 Binding(
194 194 auto_suggest.accept_or_jump_to_end,
195 195 ["c-e"],
196 196 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
197 197 ),
198 198 Binding(
199 199 auto_suggest.accept,
200 200 ["c-f"],
201 201 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
202 202 ),
203 203 Binding(
204 204 auto_suggest.accept,
205 205 ["right"],
206 206 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
207 207 ),
208 208 Binding(
209 209 auto_suggest.accept_word,
210 210 ["escape", "f"],
211 211 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
212 212 ),
213 213 Binding(
214 214 auto_suggest.accept_token,
215 215 ["c-right"],
216 216 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
217 217 ),
218 218 Binding(
219 219 auto_suggest.discard,
220 220 ["escape"],
221 221 # note this one is using `emacs_insert_mode`, not `emacs_like_insert_mode`
222 222 # as in `vi_insert_mode` we do not want `escape` to be shadowed (ever).
223 223 "has_suggestion & default_buffer_focused & emacs_insert_mode",
224 224 ),
225 225 Binding(
226 226 auto_suggest.discard,
227 227 ["delete"],
228 228 "has_suggestion & default_buffer_focused & emacs_insert_mode",
229 229 ),
230 230 Binding(
231 231 auto_suggest.swap_autosuggestion_up,
232 232 ["c-up"],
233 233 "navigable_suggestions"
234 234 " & ~has_line_above"
235 235 " & has_suggestion"
236 236 " & default_buffer_focused",
237 237 ),
238 238 Binding(
239 239 auto_suggest.swap_autosuggestion_down,
240 240 ["c-down"],
241 241 "navigable_suggestions"
242 242 " & ~has_line_below"
243 243 " & has_suggestion"
244 244 " & default_buffer_focused",
245 245 ),
246 246 Binding(
247 247 auto_suggest.up_and_update_hint,
248 248 ["c-up"],
249 249 "has_line_above & navigable_suggestions & default_buffer_focused",
250 250 ),
251 251 Binding(
252 252 auto_suggest.down_and_update_hint,
253 253 ["c-down"],
254 254 "has_line_below & navigable_suggestions & default_buffer_focused",
255 255 ),
256 256 Binding(
257 257 auto_suggest.accept_character,
258 258 ["escape", "right"],
259 259 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
260 260 ),
261 261 Binding(
262 262 auto_suggest.accept_and_move_cursor_left,
263 263 ["c-left"],
264 264 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
265 265 ),
266 266 Binding(
267 267 auto_suggest.accept_and_keep_cursor,
268 268 ["escape", "down"],
269 269 "has_suggestion & default_buffer_focused & emacs_insert_mode",
270 270 ),
271 271 Binding(
272 272 auto_suggest.backspace_and_resume_hint,
273 273 ["backspace"],
274 274 # no `has_suggestion` here to allow resuming if no suggestion
275 275 "default_buffer_focused & emacs_like_insert_mode",
276 276 ),
277 277 Binding(
278 278 auto_suggest.resume_hinting,
279 279 ["right"],
280 "default_buffer_focused & emacs_like_insert_mode",
280 "is_cursor_at_the_end_of_line"
281 " & default_buffer_focused"
282 " & emacs_like_insert_mode",
281 283 ),
282 284 ]
283 285
284 286
285 287 SIMPLE_CONTROL_BINDINGS = [
286 288 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
287 289 for key, cmd in {
288 290 "c-a": nc.beginning_of_line,
289 291 "c-b": nc.backward_char,
290 292 "c-k": nc.kill_line,
291 293 "c-w": nc.backward_kill_word,
292 294 "c-y": nc.yank,
293 295 "c-_": nc.undo,
294 296 }.items()
295 297 ]
296 298
297 299
298 300 ALT_AND_COMOBO_CONTROL_BINDINGS = [
299 301 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
300 302 for keys, cmd in {
301 303 # Control Combos
302 304 ("c-x", "c-e"): nc.edit_and_execute,
303 305 ("c-x", "e"): nc.edit_and_execute,
304 306 # Alt
305 307 ("escape", "b"): nc.backward_word,
306 308 ("escape", "c"): nc.capitalize_word,
307 309 ("escape", "d"): nc.kill_word,
308 310 ("escape", "h"): nc.backward_kill_word,
309 311 ("escape", "l"): nc.downcase_word,
310 312 ("escape", "u"): nc.uppercase_word,
311 313 ("escape", "y"): nc.yank_pop,
312 314 ("escape", "."): nc.yank_last_arg,
313 315 }.items()
314 316 ]
315 317
316 318
317 319 def add_binding(bindings: KeyBindings, binding: Binding):
318 320 bindings.add(
319 321 *binding.keys,
320 322 **({"filter": binding.filter} if binding.filter is not None else {}),
321 323 )(binding.command)
322 324
323 325
324 326 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
325 327 """Set up the prompt_toolkit keyboard shortcuts for IPython.
326 328
327 329 Parameters
328 330 ----------
329 331 shell: InteractiveShell
330 332 The current IPython shell Instance
331 333 skip: List[Binding]
332 334 Bindings to skip.
333 335
334 336 Returns
335 337 -------
336 338 KeyBindings
337 339 the keybinding instance for prompt toolkit.
338 340
339 341 """
340 342 kb = KeyBindings()
341 343 skip = skip or []
342 344 for binding in KEY_BINDINGS:
343 345 skip_this_one = False
344 346 for to_skip in skip:
345 347 if (
346 348 to_skip.command == binding.command
347 349 and to_skip.filter == binding.filter
348 350 and to_skip.keys == binding.keys
349 351 ):
350 352 skip_this_one = True
351 353 break
352 354 if skip_this_one:
353 355 continue
354 356 add_binding(kb, binding)
355 357
356 358 def get_input_mode(self):
357 359 app = get_app()
358 360 app.ttimeoutlen = shell.ttimeoutlen
359 361 app.timeoutlen = shell.timeoutlen
360 362
361 363 return self._input_mode
362 364
363 365 def set_input_mode(self, mode):
364 366 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
365 367 cursor = "\x1b[{} q".format(shape)
366 368
367 369 sys.stdout.write(cursor)
368 370 sys.stdout.flush()
369 371
370 372 self._input_mode = mode
371 373
372 374 if shell.editing_mode == "vi" and shell.modal_cursor:
373 375 ViState._input_mode = InputMode.INSERT # type: ignore
374 376 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
375 377
376 378 return kb
377 379
378 380
379 381 def reformat_and_execute(event):
380 382 """Reformat code and execute it"""
381 383 shell = get_ipython()
382 384 reformat_text_before_cursor(
383 385 event.current_buffer, event.current_buffer.document, shell
384 386 )
385 387 event.current_buffer.validate_and_handle()
386 388
387 389
388 390 def reformat_text_before_cursor(buffer, document, shell):
389 391 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
390 392 try:
391 393 formatted_text = shell.reformat_handler(text)
392 394 buffer.insert_text(formatted_text)
393 395 except Exception as e:
394 396 buffer.insert_text(text)
395 397
396 398
397 399 def handle_return_or_newline_or_execute(event):
398 400 shell = get_ipython()
399 401 if getattr(shell, "handle_return", None):
400 402 return shell.handle_return(shell)(event)
401 403 else:
402 404 return newline_or_execute_outer(shell)(event)
403 405
404 406
405 407 def newline_or_execute_outer(shell):
406 408 def newline_or_execute(event):
407 409 """When the user presses return, insert a newline or execute the code."""
408 410 b = event.current_buffer
409 411 d = b.document
410 412
411 413 if b.complete_state:
412 414 cc = b.complete_state.current_completion
413 415 if cc:
414 416 b.apply_completion(cc)
415 417 else:
416 418 b.cancel_completion()
417 419 return
418 420
419 421 # If there's only one line, treat it as if the cursor is at the end.
420 422 # See https://github.com/ipython/ipython/issues/10425
421 423 if d.line_count == 1:
422 424 check_text = d.text
423 425 else:
424 426 check_text = d.text[: d.cursor_position]
425 427 status, indent = shell.check_complete(check_text)
426 428
427 429 # if all we have after the cursor is whitespace: reformat current text
428 430 # before cursor
429 431 after_cursor = d.text[d.cursor_position :]
430 432 reformatted = False
431 433 if not after_cursor.strip():
432 434 reformat_text_before_cursor(b, d, shell)
433 435 reformatted = True
434 436 if not (
435 437 d.on_last_line
436 438 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
437 439 ):
438 440 if shell.autoindent:
439 441 b.insert_text("\n" + indent)
440 442 else:
441 443 b.insert_text("\n")
442 444 return
443 445
444 446 if (status != "incomplete") and b.accept_handler:
445 447 if not reformatted:
446 448 reformat_text_before_cursor(b, d, shell)
447 449 b.validate_and_handle()
448 450 else:
449 451 if shell.autoindent:
450 452 b.insert_text("\n" + indent)
451 453 else:
452 454 b.insert_text("\n")
453 455
454 456 return newline_or_execute
455 457
456 458
457 459 def previous_history_or_previous_completion(event):
458 460 """
459 461 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
460 462
461 463 If completer is open this still select previous completion.
462 464 """
463 465 event.current_buffer.auto_up()
464 466
465 467
466 468 def next_history_or_next_completion(event):
467 469 """
468 470 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
469 471
470 472 If completer is open this still select next completion.
471 473 """
472 474 event.current_buffer.auto_down()
473 475
474 476
475 477 def dismiss_completion(event):
476 478 """Dismiss completion"""
477 479 b = event.current_buffer
478 480 if b.complete_state:
479 481 b.cancel_completion()
480 482
481 483
482 484 def reset_buffer(event):
483 485 """Reset buffer"""
484 486 b = event.current_buffer
485 487 if b.complete_state:
486 488 b.cancel_completion()
487 489 else:
488 490 b.reset()
489 491
490 492
491 493 def reset_search_buffer(event):
492 494 """Reset search buffer"""
493 495 if event.current_buffer.document.text:
494 496 event.current_buffer.reset()
495 497 else:
496 498 event.app.layout.focus(DEFAULT_BUFFER)
497 499
498 500
499 501 def suspend_to_bg(event):
500 502 """Suspend to background"""
501 503 event.app.suspend_to_background()
502 504
503 505
504 506 def quit(event):
505 507 """
506 508 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
507 509
508 510 On platforms that support SIGQUIT, send SIGQUIT to the current process.
509 511 On other platforms, just exit the process with a message.
510 512 """
511 513 sigquit = getattr(signal, "SIGQUIT", None)
512 514 if sigquit is not None:
513 515 os.kill(0, signal.SIGQUIT)
514 516 else:
515 517 sys.exit("Quit")
516 518
517 519
518 520 def indent_buffer(event):
519 521 """Indent buffer"""
520 522 event.current_buffer.insert_text(" " * 4)
521 523
522 524
523 525 def newline_autoindent(event):
524 526 """Insert a newline after the cursor indented appropriately.
525 527
526 528 Fancier version of former ``newline_with_copy_margin`` which should
527 529 compute the correct indentation of the inserted line. That is to say, indent
528 530 by 4 extra space after a function definition, class definition, context
529 531 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
530 532 """
531 533 shell = get_ipython()
532 534 inputsplitter = shell.input_transformer_manager
533 535 b = event.current_buffer
534 536 d = b.document
535 537
536 538 if b.complete_state:
537 539 b.cancel_completion()
538 540 text = d.text[: d.cursor_position] + "\n"
539 541 _, indent = inputsplitter.check_complete(text)
540 542 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
541 543
542 544
543 545 def open_input_in_editor(event):
544 546 """Open code from input in external editor"""
545 547 event.app.current_buffer.open_in_editor()
546 548
547 549
548 550 if sys.platform == "win32":
549 551 from IPython.core.error import TryNext
550 552 from IPython.lib.clipboard import (
551 553 ClipboardEmpty,
552 554 tkinter_clipboard_get,
553 555 win32_clipboard_get,
554 556 )
555 557
556 558 @undoc
557 559 def win_paste(event):
558 560 try:
559 561 text = win32_clipboard_get()
560 562 except TryNext:
561 563 try:
562 564 text = tkinter_clipboard_get()
563 565 except (TryNext, ClipboardEmpty):
564 566 return
565 567 except ClipboardEmpty:
566 568 return
567 569 event.current_buffer.insert_text(text.replace("\t", " " * 4))
568 570
569 571 else:
570 572
571 573 @undoc
572 574 def win_paste(event):
573 575 """Stub used on other platforms"""
574 576 pass
575 577
576 578
577 579 KEY_BINDINGS = [
578 580 Binding(
579 581 handle_return_or_newline_or_execute,
580 582 ["enter"],
581 583 "default_buffer_focused & ~has_selection & insert_mode",
582 584 ),
583 585 Binding(
584 586 reformat_and_execute,
585 587 ["escape", "enter"],
586 588 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
587 589 ),
588 590 Binding(quit, ["c-\\"]),
589 591 Binding(
590 592 previous_history_or_previous_completion,
591 593 ["c-p"],
592 594 "vi_insert_mode & default_buffer_focused",
593 595 ),
594 596 Binding(
595 597 next_history_or_next_completion,
596 598 ["c-n"],
597 599 "vi_insert_mode & default_buffer_focused",
598 600 ),
599 601 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
600 602 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
601 603 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
602 604 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
603 605 Binding(
604 606 indent_buffer,
605 607 ["tab"], # Ctrl+I == Tab
606 608 "default_buffer_focused"
607 609 " & ~has_selection"
608 610 " & insert_mode"
609 611 " & cursor_in_leading_ws",
610 612 ),
611 613 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
612 614 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
613 615 *AUTO_MATCH_BINDINGS,
614 616 *AUTO_SUGGEST_BINDINGS,
615 617 Binding(
616 618 display_completions_like_readline,
617 619 ["c-i"],
618 620 "readline_like_completions"
619 621 " & default_buffer_focused"
620 622 " & ~has_selection"
621 623 " & insert_mode"
622 624 " & ~cursor_in_leading_ws",
623 625 ),
624 626 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
625 627 *SIMPLE_CONTROL_BINDINGS,
626 628 *ALT_AND_COMOBO_CONTROL_BINDINGS,
627 629 ]
@@ -1,282 +1,290 b''
1 1 """
2 2 Filters restricting scope of IPython Terminal shortcuts.
3 3 """
4 4
5 5 # Copyright (c) IPython Development Team.
6 6 # Distributed under the terms of the Modified BSD License.
7 7
8 8 import ast
9 9 import re
10 10 import signal
11 11 import sys
12 12 from typing import Callable, Dict, Union
13 13
14 14 from prompt_toolkit.application.current import get_app
15 15 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
16 16 from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions
17 17 from prompt_toolkit.filters import has_focus as has_focus_impl
18 18 from prompt_toolkit.filters import (
19 19 Always,
20 20 Never,
21 21 has_selection,
22 22 has_suggestion,
23 23 vi_insert_mode,
24 24 vi_mode,
25 25 )
26 26 from prompt_toolkit.layout.layout import FocusableElement
27 27
28 28 from IPython.core.getipython import get_ipython
29 29 from IPython.core.guarded_eval import _find_dunder, BINARY_OP_DUNDERS, UNARY_OP_DUNDERS
30 30 from IPython.terminal.shortcuts import auto_suggest
31 31 from IPython.utils.decorators import undoc
32 32
33 33
34 34 @undoc
35 35 @Condition
36 36 def cursor_in_leading_ws():
37 37 before = get_app().current_buffer.document.current_line_before_cursor
38 38 return (not before) or before.isspace()
39 39
40 40
41 41 def has_focus(value: FocusableElement):
42 42 """Wrapper around has_focus adding a nice `__name__` to tester function"""
43 43 tester = has_focus_impl(value).func
44 44 tester.__name__ = f"is_focused({value})"
45 45 return Condition(tester)
46 46
47 47
48 48 @undoc
49 49 @Condition
50 50 def has_line_below() -> bool:
51 51 document = get_app().current_buffer.document
52 52 return document.cursor_position_row < len(document.lines) - 1
53 53
54 54
55 55 @undoc
56 56 @Condition
57 def is_cursor_at_the_end_of_line() -> bool:
58 document = get_app().current_buffer.document
59 return document.is_cursor_at_the_end_of_line
60
61
62 @undoc
63 @Condition
57 64 def has_line_above() -> bool:
58 65 document = get_app().current_buffer.document
59 66 return document.cursor_position_row != 0
60 67
61 68
62 69 @Condition
63 70 def ebivim():
64 71 shell = get_ipython()
65 72 return shell.emacs_bindings_in_vi_insert_mode
66 73
67 74
68 75 @Condition
69 76 def supports_suspend():
70 77 return hasattr(signal, "SIGTSTP")
71 78
72 79
73 80 @Condition
74 81 def auto_match():
75 82 shell = get_ipython()
76 83 return shell.auto_match
77 84
78 85
79 86 def all_quotes_paired(quote, buf):
80 87 paired = True
81 88 i = 0
82 89 while i < len(buf):
83 90 c = buf[i]
84 91 if c == quote:
85 92 paired = not paired
86 93 elif c == "\\":
87 94 i += 1
88 95 i += 1
89 96 return paired
90 97
91 98
92 99 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
93 100 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
94 101
95 102
96 103 def preceding_text(pattern: Union[str, Callable]):
97 104 if pattern in _preceding_text_cache:
98 105 return _preceding_text_cache[pattern]
99 106
100 107 if callable(pattern):
101 108
102 109 def _preceding_text():
103 110 app = get_app()
104 111 before_cursor = app.current_buffer.document.current_line_before_cursor
105 112 # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
106 113 return bool(pattern(before_cursor)) # type: ignore[operator]
107 114
108 115 else:
109 116 m = re.compile(pattern)
110 117
111 118 def _preceding_text():
112 119 app = get_app()
113 120 before_cursor = app.current_buffer.document.current_line_before_cursor
114 121 return bool(m.match(before_cursor))
115 122
116 123 _preceding_text.__name__ = f"preceding_text({pattern!r})"
117 124
118 125 condition = Condition(_preceding_text)
119 126 _preceding_text_cache[pattern] = condition
120 127 return condition
121 128
122 129
123 130 def following_text(pattern):
124 131 try:
125 132 return _following_text_cache[pattern]
126 133 except KeyError:
127 134 pass
128 135 m = re.compile(pattern)
129 136
130 137 def _following_text():
131 138 app = get_app()
132 139 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
133 140
134 141 _following_text.__name__ = f"following_text({pattern!r})"
135 142
136 143 condition = Condition(_following_text)
137 144 _following_text_cache[pattern] = condition
138 145 return condition
139 146
140 147
141 148 @Condition
142 149 def not_inside_unclosed_string():
143 150 app = get_app()
144 151 s = app.current_buffer.document.text_before_cursor
145 152 # remove escaped quotes
146 153 s = s.replace('\\"', "").replace("\\'", "")
147 154 # remove triple-quoted string literals
148 155 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
149 156 # remove single-quoted string literals
150 157 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
151 158 return not ('"' in s or "'" in s)
152 159
153 160
154 161 @Condition
155 162 def navigable_suggestions():
156 163 shell = get_ipython()
157 164 return isinstance(shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory)
158 165
159 166
160 167 @Condition
161 168 def readline_like_completions():
162 169 shell = get_ipython()
163 170 return shell.display_completions == "readlinelike"
164 171
165 172
166 173 @Condition
167 174 def is_windows_os():
168 175 return sys.platform == "win32"
169 176
170 177
171 178 # these one is callable and re-used multiple times hence needs to be
172 179 # only defined once beforhand so that transforming back to human-readable
173 180 # names works well in the documentation.
174 181 default_buffer_focused = has_focus(DEFAULT_BUFFER)
175 182
176 183 KEYBINDING_FILTERS = {
177 184 "always": Always(),
178 185 # never is used for exposing commands which have no default keybindings
179 186 "never": Never(),
180 187 "has_line_below": has_line_below,
181 188 "has_line_above": has_line_above,
189 "is_cursor_at_the_end_of_line": is_cursor_at_the_end_of_line,
182 190 "has_selection": has_selection,
183 191 "has_suggestion": has_suggestion,
184 192 "vi_mode": vi_mode,
185 193 "vi_insert_mode": vi_insert_mode,
186 194 "emacs_insert_mode": emacs_insert_mode,
187 195 # https://github.com/ipython/ipython/pull/12603 argued for inclusion of
188 196 # emacs key bindings with a configurable `emacs_bindings_in_vi_insert_mode`
189 197 # toggle; when the toggle is on user can access keybindigns like `ctrl + e`
190 198 # in vi insert mode. Because some of the emacs bindings involve `escape`
191 199 # followed by another key, e.g. `escape` followed by `f`, prompt-toolkit
192 200 # needs to wait to see if there will be another character typed in before
193 201 # executing pure `escape` keybinding; in vi insert mode `escape` switches to
194 202 # command mode which is common and performance critical action for vi users.
195 203 # To avoid the delay users employ a workaround:
196 204 # https://github.com/ipython/ipython/issues/13443#issuecomment-1032753703
197 205 # which involves switching `emacs_bindings_in_vi_insert_mode` off.
198 206 #
199 207 # For the workaround to work:
200 208 # 1) end users need to toggle `emacs_bindings_in_vi_insert_mode` off
201 209 # 2) all keybindings which would involve `escape` need to respect that
202 210 # toggle by including either:
203 211 # - `vi_insert_mode & ebivim` for actions which have emacs keybindings
204 212 # predefined upstream in prompt-toolkit, or
205 213 # - `emacs_like_insert_mode` for actions which do not have existing
206 214 # emacs keybindings predefined upstream (or need overriding of the
207 215 # upstream bindings to modify behaviour), defined below.
208 216 "emacs_like_insert_mode": (vi_insert_mode & ebivim) | emacs_insert_mode,
209 217 "has_completions": has_completions,
210 218 "insert_mode": vi_insert_mode | emacs_insert_mode,
211 219 "default_buffer_focused": default_buffer_focused,
212 220 "search_buffer_focused": has_focus(SEARCH_BUFFER),
213 221 # `ebivim` stands for emacs bindings in vi insert mode
214 222 "ebivim": ebivim,
215 223 "supports_suspend": supports_suspend,
216 224 "is_windows_os": is_windows_os,
217 225 "auto_match": auto_match,
218 226 "focused_insert": (vi_insert_mode | emacs_insert_mode) & default_buffer_focused,
219 227 "not_inside_unclosed_string": not_inside_unclosed_string,
220 228 "readline_like_completions": readline_like_completions,
221 229 "preceded_by_paired_double_quotes": preceding_text(
222 230 lambda line: all_quotes_paired('"', line)
223 231 ),
224 232 "preceded_by_paired_single_quotes": preceding_text(
225 233 lambda line: all_quotes_paired("'", line)
226 234 ),
227 235 "preceded_by_raw_str_prefix": preceding_text(r".*(r|R)[\"'](-*)$"),
228 236 "preceded_by_two_double_quotes": preceding_text(r'^.*""$'),
229 237 "preceded_by_two_single_quotes": preceding_text(r"^.*''$"),
230 238 "followed_by_closing_paren_or_end": following_text(r"[,)}\]]|$"),
231 239 "preceded_by_opening_round_paren": preceding_text(r".*\($"),
232 240 "preceded_by_opening_bracket": preceding_text(r".*\[$"),
233 241 "preceded_by_opening_brace": preceding_text(r".*\{$"),
234 242 "preceded_by_double_quote": preceding_text('.*"$'),
235 243 "preceded_by_single_quote": preceding_text(r".*'$"),
236 244 "followed_by_closing_round_paren": following_text(r"^\)"),
237 245 "followed_by_closing_bracket": following_text(r"^\]"),
238 246 "followed_by_closing_brace": following_text(r"^\}"),
239 247 "followed_by_double_quote": following_text('^"'),
240 248 "followed_by_single_quote": following_text("^'"),
241 249 "navigable_suggestions": navigable_suggestions,
242 250 "cursor_in_leading_ws": cursor_in_leading_ws,
243 251 }
244 252
245 253
246 254 def eval_node(node: Union[ast.AST, None]):
247 255 if node is None:
248 256 return None
249 257 if isinstance(node, ast.Expression):
250 258 return eval_node(node.body)
251 259 if isinstance(node, ast.BinOp):
252 260 left = eval_node(node.left)
253 261 right = eval_node(node.right)
254 262 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
255 263 if dunders:
256 264 return getattr(left, dunders[0])(right)
257 265 raise ValueError(f"Unknown binary operation: {node.op}")
258 266 if isinstance(node, ast.UnaryOp):
259 267 value = eval_node(node.operand)
260 268 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
261 269 if dunders:
262 270 return getattr(value, dunders[0])()
263 271 raise ValueError(f"Unknown unary operation: {node.op}")
264 272 if isinstance(node, ast.Name):
265 273 if node.id in KEYBINDING_FILTERS:
266 274 return KEYBINDING_FILTERS[node.id]
267 275 else:
268 276 sep = "\n - "
269 277 known_filters = sep.join(sorted(KEYBINDING_FILTERS))
270 278 raise NameError(
271 279 f"{node.id} is not a known shortcut filter."
272 280 f" Known filters are: {sep}{known_filters}."
273 281 )
274 282 raise ValueError("Unhandled node", ast.dump(node))
275 283
276 284
277 285 def filter_from_string(code: str):
278 286 expression = ast.parse(code, mode="eval")
279 287 return eval_node(expression)
280 288
281 289
282 290 __all__ = ["KEYBINDING_FILTERS", "filter_from_string"]
General Comments 0
You need to be logged in to leave comments. Login now