##// END OF EJS Templates
Misc review cleanup....
Matthias Bussonnier -
Show More
@@ -1,638 +1,640 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 import warnings
9 import os
10 import re
10 11 import signal
11 12 import sys
12 import re
13 import os
13 import warnings
14 14 from typing import Callable, Dict, Union
15 15
16
17 16 from prompt_toolkit.application.current import get_app
18 17 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
18 from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions
19 from prompt_toolkit.filters import has_focus as has_focus_impl
19 20 from prompt_toolkit.filters import (
20 has_focus as has_focus_impl,
21 21 has_selection,
22 Condition,
22 has_suggestion,
23 23 vi_insert_mode,
24 emacs_insert_mode,
25 has_completions,
26 24 vi_mode,
27 25 )
26 from prompt_toolkit.key_binding import KeyBindings
27 from prompt_toolkit.key_binding.bindings import named_commands as nc
28 28 from prompt_toolkit.key_binding.bindings.completion import (
29 29 display_completions_like_readline,
30 30 )
31 from prompt_toolkit.key_binding import KeyBindings
32 from prompt_toolkit.key_binding.bindings import named_commands as nc
33 31 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
34 32 from prompt_toolkit.layout.layout import FocusableElement
35 33
34 from IPython.terminal.shortcuts import auto_match as match
35 from IPython.terminal.shortcuts import auto_suggest
36 36 from IPython.utils.decorators import undoc
37 from . import auto_match as match, auto_suggest
38
39 37
40 38 __all__ = ["create_ipython_shortcuts"]
41 39
42 40
43 try:
44 # only added in 3.0.30
45 from prompt_toolkit.filters import has_suggestion
46 except ImportError:
47
48 @undoc
49 @Condition
50 def has_suggestion():
51 buffer = get_app().current_buffer
52 return buffer.suggestion is not None and buffer.suggestion.text != ""
53
54
55 41 @undoc
56 42 @Condition
57 43 def cursor_in_leading_ws():
58 44 before = get_app().current_buffer.document.current_line_before_cursor
59 45 return (not before) or before.isspace()
60 46
61 47
62 48 def has_focus(value: FocusableElement):
63 49 """Wrapper around has_focus adding a nice `__name__` to tester function"""
64 50 tester = has_focus_impl(value).func
65 51 tester.__name__ = f"is_focused({value})"
66 52 return Condition(tester)
67 53
68 54
69 def create_ipython_shortcuts(shell, for_all_platforms: bool = False):
70 """Set up the prompt_toolkit keyboard shortcuts for IPython."""
55 def create_ipython_shortcuts(shell, for_all_platforms: bool = False) -> KeyBindings:
56 """Set up the prompt_toolkit keyboard shortcuts for IPython.
57
58 Parameters
59 ----------
60 shell: InteractiveShell
61 The current IPython shell Instance
62 for_all_platforms: bool (default false)
63 This parameter is mostly used in generating the documentation
64 to create the shortcut binding for all the platforms, and export
65 them.
66
67 Returns
68 -------
69 KeyBindings
70 the keybinding instance for prompt toolkit.
71
72 """
71 73 # Warning: if possible, do NOT define handler functions in the locals
72 74 # scope of this function, instead define functions in the global
73 75 # scope, or a separate module, and include a user-friendly docstring
74 76 # describing the action.
75 77
76 78 kb = KeyBindings()
77 79 insert_mode = vi_insert_mode | emacs_insert_mode
78 80
79 81 if getattr(shell, "handle_return", None):
80 82 return_handler = shell.handle_return(shell)
81 83 else:
82 84 return_handler = newline_or_execute_outer(shell)
83 85
84 86 kb.add("enter", filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode))(
85 87 return_handler
86 88 )
87 89
88 90 @Condition
89 91 def ebivim():
90 92 return shell.emacs_bindings_in_vi_insert_mode
91 93
92 94 @kb.add(
93 95 "escape",
94 96 "enter",
95 97 filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode & ebivim),
96 98 )
97 99 def reformat_and_execute(event):
98 100 """Reformat code and execute it"""
99 101 reformat_text_before_cursor(
100 102 event.current_buffer, event.current_buffer.document, shell
101 103 )
102 104 event.current_buffer.validate_and_handle()
103 105
104 106 kb.add("c-\\")(quit)
105 107
106 108 kb.add("c-p", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))(
107 109 previous_history_or_previous_completion
108 110 )
109 111
110 112 kb.add("c-n", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))(
111 113 next_history_or_next_completion
112 114 )
113 115
114 116 kb.add("c-g", filter=(has_focus(DEFAULT_BUFFER) & has_completions))(
115 117 dismiss_completion
116 118 )
117 119
118 120 kb.add("c-c", filter=has_focus(DEFAULT_BUFFER))(reset_buffer)
119 121
120 122 kb.add("c-c", filter=has_focus(SEARCH_BUFFER))(reset_search_buffer)
121 123
122 124 supports_suspend = Condition(lambda: hasattr(signal, "SIGTSTP"))
123 125 kb.add("c-z", filter=supports_suspend)(suspend_to_bg)
124 126
125 127 # Ctrl+I == Tab
126 128 kb.add(
127 129 "tab",
128 130 filter=(
129 131 has_focus(DEFAULT_BUFFER)
130 132 & ~has_selection
131 133 & insert_mode
132 134 & cursor_in_leading_ws
133 135 ),
134 136 )(indent_buffer)
135 137 kb.add("c-o", filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode))(
136 138 newline_autoindent_outer(shell.input_transformer_manager)
137 139 )
138 140
139 141 kb.add("f2", filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor)
140 142
141 143 @Condition
142 144 def auto_match():
143 145 return shell.auto_match
144 146
145 147 def all_quotes_paired(quote, buf):
146 148 paired = True
147 149 i = 0
148 150 while i < len(buf):
149 151 c = buf[i]
150 152 if c == quote:
151 153 paired = not paired
152 154 elif c == "\\":
153 155 i += 1
154 156 i += 1
155 157 return paired
156 158
157 159 focused_insert = (vi_insert_mode | emacs_insert_mode) & has_focus(DEFAULT_BUFFER)
158 160 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
159 161 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
160 162
161 163 def preceding_text(pattern: Union[str, Callable]):
162 164 if pattern in _preceding_text_cache:
163 165 return _preceding_text_cache[pattern]
164 166
165 167 if callable(pattern):
166 168 def _preceding_text():
167 169 app = get_app()
168 170 before_cursor = app.current_buffer.document.current_line_before_cursor
169 171 # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
170 172 return bool(pattern(before_cursor)) # type: ignore[operator]
171 173
172 174 else:
173 175 m = re.compile(pattern)
174 176
175 177 def _preceding_text():
176 178 app = get_app()
177 179 before_cursor = app.current_buffer.document.current_line_before_cursor
178 180 return bool(m.match(before_cursor))
179 181
180 182 _preceding_text.__name__ = f"preceding_text({pattern!r})"
181 183
182 184 condition = Condition(_preceding_text)
183 185 _preceding_text_cache[pattern] = condition
184 186 return condition
185 187
186 188 def following_text(pattern):
187 189 try:
188 190 return _following_text_cache[pattern]
189 191 except KeyError:
190 192 pass
191 193 m = re.compile(pattern)
192 194
193 195 def _following_text():
194 196 app = get_app()
195 197 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
196 198
197 199 _following_text.__name__ = f"following_text({pattern!r})"
198 200
199 201 condition = Condition(_following_text)
200 202 _following_text_cache[pattern] = condition
201 203 return condition
202 204
203 205 @Condition
204 206 def not_inside_unclosed_string():
205 207 app = get_app()
206 208 s = app.current_buffer.document.text_before_cursor
207 209 # remove escaped quotes
208 210 s = s.replace('\\"', "").replace("\\'", "")
209 211 # remove triple-quoted string literals
210 212 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
211 213 # remove single-quoted string literals
212 214 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
213 215 return not ('"' in s or "'" in s)
214 216
215 217 # auto match
216 218 auto_match_parens = {"(": match.parenthesis, "[": match.brackets, "{": match.braces}
217 219 for key, cmd in auto_match_parens.items():
218 220 kb.add(key, filter=focused_insert & auto_match & following_text(r"[,)}\]]|$"))(
219 221 cmd
220 222 )
221 223
222 224 kb.add(
223 225 '"',
224 226 filter=focused_insert
225 227 & auto_match
226 228 & not_inside_unclosed_string
227 229 & preceding_text(lambda line: all_quotes_paired('"', line))
228 230 & following_text(r"[,)}\]]|$"),
229 231 )(match.double_quote)
230 232
231 233 kb.add(
232 234 "'",
233 235 filter=focused_insert
234 236 & auto_match
235 237 & not_inside_unclosed_string
236 238 & preceding_text(lambda line: all_quotes_paired("'", line))
237 239 & following_text(r"[,)}\]]|$"),
238 240 )(match.single_quote)
239 241
240 242 kb.add(
241 243 '"',
242 244 filter=focused_insert
243 245 & auto_match
244 246 & not_inside_unclosed_string
245 247 & preceding_text(r'^.*""$'),
246 248 )(match.docstring_double_quotes)
247 249
248 250 kb.add(
249 251 "'",
250 252 filter=focused_insert
251 253 & auto_match
252 254 & not_inside_unclosed_string
253 255 & preceding_text(r"^.*''$"),
254 256 )(match.docstring_single_quotes)
255 257
256 258 # raw string
257 259 auto_match_parens_raw_string = {
258 260 "(": match.raw_string_parenthesis,
259 261 "[": match.raw_string_bracket,
260 262 "{": match.raw_string_braces,
261 263 }
262 264 for key, cmd in auto_match_parens_raw_string.items():
263 265 kb.add(
264 266 key,
265 267 filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$"),
266 268 )(cmd)
267 269
268 270 # just move cursor
269 271 kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)"))(
270 272 match.skip_over
271 273 )
272 274 kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]"))(
273 275 match.skip_over
274 276 )
275 277 kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}"))(
276 278 match.skip_over
277 279 )
278 280 kb.add('"', filter=focused_insert & auto_match & following_text('^"'))(
279 281 match.skip_over
280 282 )
281 283 kb.add("'", filter=focused_insert & auto_match & following_text("^'"))(
282 284 match.skip_over
283 285 )
284 286
285 287 kb.add(
286 288 "backspace",
287 289 filter=focused_insert
288 290 & preceding_text(r".*\($")
289 291 & auto_match
290 292 & following_text(r"^\)"),
291 293 )(match.delete_pair)
292 294 kb.add(
293 295 "backspace",
294 296 filter=focused_insert
295 297 & preceding_text(r".*\[$")
296 298 & auto_match
297 299 & following_text(r"^\]"),
298 300 )(match.delete_pair)
299 301 kb.add(
300 302 "backspace",
301 303 filter=focused_insert
302 304 & preceding_text(r".*\{$")
303 305 & auto_match
304 306 & following_text(r"^\}"),
305 307 )(match.delete_pair)
306 308 kb.add(
307 309 "backspace",
308 310 filter=focused_insert
309 311 & preceding_text('.*"$')
310 312 & auto_match
311 313 & following_text('^"'),
312 314 )(match.delete_pair)
313 315 kb.add(
314 316 "backspace",
315 317 filter=focused_insert
316 318 & preceding_text(r".*'$")
317 319 & auto_match
318 320 & following_text(r"^'"),
319 321 )(match.delete_pair)
320 322
321 323 if shell.display_completions == "readlinelike":
322 324 kb.add(
323 325 "c-i",
324 326 filter=(
325 327 has_focus(DEFAULT_BUFFER)
326 328 & ~has_selection
327 329 & insert_mode
328 330 & ~cursor_in_leading_ws
329 331 ),
330 332 )(display_completions_like_readline)
331 333
332 334 if sys.platform == "win32" or for_all_platforms:
333 335 kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste)
334 336
335 337 focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode
336 338
337 339 # autosuggestions
338 340 kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))(
339 341 auto_suggest.accept_in_vi_insert_mode
340 342 )
341 343 kb.add("c-e", filter=focused_insert_vi & ebivim)(
342 344 auto_suggest.accept_in_vi_insert_mode
343 345 )
344 346 kb.add("c-f", filter=focused_insert_vi)(auto_suggest.accept)
345 347 kb.add("escape", "f", filter=focused_insert_vi & ebivim)(auto_suggest.accept_word)
346 348 kb.add("c-right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
347 349 auto_suggest.accept_token
348 350 )
349 351 from functools import partial
350 352
351 353 kb.add("up", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
352 354 auto_suggest.swap_autosuggestion_up(shell.auto_suggest)
353 355 )
354 356 kb.add("down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
355 357 auto_suggest.swap_autosuggestion_down(shell.auto_suggest)
356 358 )
357 359 kb.add("right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
358 360 auto_suggest.accept_character
359 361 )
360 362 kb.add("left", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
361 363 auto_suggest.accept_and_move_cursor_left
362 364 )
363 365 kb.add("c-down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
364 366 auto_suggest.accept_and_keep_cursor
365 367 )
366 368 kb.add("backspace", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
367 369 auto_suggest.backspace_and_resume_hint
368 370 )
369 371
370 372 # Simple Control keybindings
371 373 key_cmd_dict = {
372 374 "c-a": nc.beginning_of_line,
373 375 "c-b": nc.backward_char,
374 376 "c-k": nc.kill_line,
375 377 "c-w": nc.backward_kill_word,
376 378 "c-y": nc.yank,
377 379 "c-_": nc.undo,
378 380 }
379 381
380 382 for key, cmd in key_cmd_dict.items():
381 383 kb.add(key, filter=focused_insert_vi & ebivim)(cmd)
382 384
383 385 # Alt and Combo Control keybindings
384 386 keys_cmd_dict = {
385 387 # Control Combos
386 388 ("c-x", "c-e"): nc.edit_and_execute,
387 389 ("c-x", "e"): nc.edit_and_execute,
388 390 # Alt
389 391 ("escape", "b"): nc.backward_word,
390 392 ("escape", "c"): nc.capitalize_word,
391 393 ("escape", "d"): nc.kill_word,
392 394 ("escape", "h"): nc.backward_kill_word,
393 395 ("escape", "l"): nc.downcase_word,
394 396 ("escape", "u"): nc.uppercase_word,
395 397 ("escape", "y"): nc.yank_pop,
396 398 ("escape", "."): nc.yank_last_arg,
397 399 }
398 400
399 401 for keys, cmd in keys_cmd_dict.items():
400 402 kb.add(*keys, filter=focused_insert_vi & ebivim)(cmd)
401 403
402 404 def get_input_mode(self):
403 405 app = get_app()
404 406 app.ttimeoutlen = shell.ttimeoutlen
405 407 app.timeoutlen = shell.timeoutlen
406 408
407 409 return self._input_mode
408 410
409 411 def set_input_mode(self, mode):
410 412 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
411 413 cursor = "\x1b[{} q".format(shape)
412 414
413 415 sys.stdout.write(cursor)
414 416 sys.stdout.flush()
415 417
416 418 self._input_mode = mode
417 419
418 420 if shell.editing_mode == "vi" and shell.modal_cursor:
419 421 ViState._input_mode = InputMode.INSERT # type: ignore
420 422 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
421 423
422 424 return kb
423 425
424 426
425 427 def reformat_text_before_cursor(buffer, document, shell):
426 428 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
427 429 try:
428 430 formatted_text = shell.reformat_handler(text)
429 431 buffer.insert_text(formatted_text)
430 432 except Exception as e:
431 433 buffer.insert_text(text)
432 434
433 435
434 436 def newline_or_execute_outer(shell):
435 437 def newline_or_execute(event):
436 438 """When the user presses return, insert a newline or execute the code."""
437 439 b = event.current_buffer
438 440 d = b.document
439 441
440 442 if b.complete_state:
441 443 cc = b.complete_state.current_completion
442 444 if cc:
443 445 b.apply_completion(cc)
444 446 else:
445 447 b.cancel_completion()
446 448 return
447 449
448 450 # If there's only one line, treat it as if the cursor is at the end.
449 451 # See https://github.com/ipython/ipython/issues/10425
450 452 if d.line_count == 1:
451 453 check_text = d.text
452 454 else:
453 455 check_text = d.text[: d.cursor_position]
454 456 status, indent = shell.check_complete(check_text)
455 457
456 458 # if all we have after the cursor is whitespace: reformat current text
457 459 # before cursor
458 460 after_cursor = d.text[d.cursor_position :]
459 461 reformatted = False
460 462 if not after_cursor.strip():
461 463 reformat_text_before_cursor(b, d, shell)
462 464 reformatted = True
463 465 if not (
464 466 d.on_last_line
465 467 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
466 468 ):
467 469 if shell.autoindent:
468 470 b.insert_text("\n" + indent)
469 471 else:
470 472 b.insert_text("\n")
471 473 return
472 474
473 475 if (status != "incomplete") and b.accept_handler:
474 476 if not reformatted:
475 477 reformat_text_before_cursor(b, d, shell)
476 478 b.validate_and_handle()
477 479 else:
478 480 if shell.autoindent:
479 481 b.insert_text("\n" + indent)
480 482 else:
481 483 b.insert_text("\n")
482 484
483 485 newline_or_execute.__qualname__ = "newline_or_execute"
484 486
485 487 return newline_or_execute
486 488
487 489
488 490 def previous_history_or_previous_completion(event):
489 491 """
490 492 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
491 493
492 494 If completer is open this still select previous completion.
493 495 """
494 496 event.current_buffer.auto_up()
495 497
496 498
497 499 def next_history_or_next_completion(event):
498 500 """
499 501 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
500 502
501 503 If completer is open this still select next completion.
502 504 """
503 505 event.current_buffer.auto_down()
504 506
505 507
506 508 def dismiss_completion(event):
507 509 """Dismiss completion"""
508 510 b = event.current_buffer
509 511 if b.complete_state:
510 512 b.cancel_completion()
511 513
512 514
513 515 def reset_buffer(event):
514 516 """Reset buffer"""
515 517 b = event.current_buffer
516 518 if b.complete_state:
517 519 b.cancel_completion()
518 520 else:
519 521 b.reset()
520 522
521 523
522 524 def reset_search_buffer(event):
523 525 """Reset search buffer"""
524 526 if event.current_buffer.document.text:
525 527 event.current_buffer.reset()
526 528 else:
527 529 event.app.layout.focus(DEFAULT_BUFFER)
528 530
529 531
530 532 def suspend_to_bg(event):
531 533 """Suspend to background"""
532 534 event.app.suspend_to_background()
533 535
534 536
535 537 def quit(event):
536 538 """
537 539 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
538 540
539 541 On platforms that support SIGQUIT, send SIGQUIT to the current process.
540 542 On other platforms, just exit the process with a message.
541 543 """
542 544 sigquit = getattr(signal, "SIGQUIT", None)
543 545 if sigquit is not None:
544 546 os.kill(0, signal.SIGQUIT)
545 547 else:
546 548 sys.exit("Quit")
547 549
548 550
549 551 def indent_buffer(event):
550 552 """Indent buffer"""
551 553 event.current_buffer.insert_text(" " * 4)
552 554
553 555
554 556 @undoc
555 557 def newline_with_copy_margin(event):
556 558 """
557 559 DEPRECATED since IPython 6.0
558 560
559 561 See :any:`newline_autoindent_outer` for a replacement.
560 562
561 563 Preserve margin and cursor position when using
562 564 Control-O to insert a newline in EMACS mode
563 565 """
564 566 warnings.warn(
565 567 "`newline_with_copy_margin(event)` is deprecated since IPython 6.0. "
566 568 "see `newline_autoindent_outer(shell)(event)` for a replacement.",
567 569 DeprecationWarning,
568 570 stacklevel=2,
569 571 )
570 572
571 573 b = event.current_buffer
572 574 cursor_start_pos = b.document.cursor_position_col
573 575 b.newline(copy_margin=True)
574 576 b.cursor_up(count=1)
575 577 cursor_end_pos = b.document.cursor_position_col
576 578 if cursor_start_pos != cursor_end_pos:
577 579 pos_diff = cursor_start_pos - cursor_end_pos
578 580 b.cursor_right(count=pos_diff)
579 581
580 582
581 583 def newline_autoindent_outer(inputsplitter) -> Callable[..., None]:
582 584 """
583 585 Return a function suitable for inserting a indented newline after the cursor.
584 586
585 587 Fancier version of deprecated ``newline_with_copy_margin`` which should
586 588 compute the correct indentation of the inserted line. That is to say, indent
587 589 by 4 extra space after a function definition, class definition, context
588 590 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
589 591 """
590 592
591 593 def newline_autoindent(event):
592 594 """Insert a newline after the cursor indented appropriately."""
593 595 b = event.current_buffer
594 596 d = b.document
595 597
596 598 if b.complete_state:
597 599 b.cancel_completion()
598 600 text = d.text[: d.cursor_position] + "\n"
599 601 _, indent = inputsplitter.check_complete(text)
600 602 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
601 603
602 604 newline_autoindent.__qualname__ = "newline_autoindent"
603 605
604 606 return newline_autoindent
605 607
606 608
607 609 def open_input_in_editor(event):
608 610 """Open code from input in external editor"""
609 611 event.app.current_buffer.open_in_editor()
610 612
611 613
612 614 if sys.platform == "win32":
613 615 from IPython.core.error import TryNext
614 616 from IPython.lib.clipboard import (
615 617 ClipboardEmpty,
616 win32_clipboard_get,
617 618 tkinter_clipboard_get,
619 win32_clipboard_get,
618 620 )
619 621
620 622 @undoc
621 623 def win_paste(event):
622 624 try:
623 625 text = win32_clipboard_get()
624 626 except TryNext:
625 627 try:
626 628 text = tkinter_clipboard_get()
627 629 except (TryNext, ClipboardEmpty):
628 630 return
629 631 except ClipboardEmpty:
630 632 return
631 633 event.current_buffer.insert_text(text.replace("\t", " " * 4))
632 634
633 635 else:
634 636
635 637 @undoc
636 638 def win_paste(event):
637 639 """Stub used when auto-generating shortcuts for documentation"""
638 640 pass
@@ -1,308 +1,308 b''
1 1 import re
2 2 import tokenize
3 3 from io import StringIO
4 4 from typing import Callable, List, Optional, Union, Generator, Tuple
5 5
6 6 from prompt_toolkit.buffer import Buffer
7 7 from prompt_toolkit.key_binding import KeyPressEvent
8 8 from prompt_toolkit.key_binding.bindings import named_commands as nc
9 9 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
10 10 from prompt_toolkit.document import Document
11 11 from prompt_toolkit.history import History
12 12 from prompt_toolkit.shortcuts import PromptSession
13 13
14 14 from IPython.utils.tokenutil import generate_tokens
15 15
16 16
17 17 def _get_query(document: Document):
18 18 return document.text.rsplit("\n", 1)[-1]
19 19
20 20
21 21 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
22 22 """
23 23 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
24 24 suggestion from history. To do so it remembers the current position, but it
25 25 state need to carefully be cleared on the right events.
26 26 """
27 27
28 28 def __init__(
29 29 self,
30 30 ):
31 31 self.skip_lines = 0
32 32 self._connected_apps = []
33 33
34 34 def reset_history_position(self, _: Buffer):
35 35 self.skip_lines = 0
36 36
37 37 def disconnect(self):
38 38 for pt_app in self._connected_apps:
39 39 text_insert_event = pt_app.default_buffer.on_text_insert
40 40 text_insert_event.remove_handler(self.reset_history_position)
41 41
42 42 def connect(self, pt_app: PromptSession):
43 43 self._connected_apps.append(pt_app)
44 44 # note: `on_text_changed` could be used for a bit different behaviour
45 45 # on character deletion (i.e. reseting history position on backspace)
46 46 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
47 47
48 48 def get_suggestion(
49 49 self, buffer: Buffer, document: Document
50 50 ) -> Optional[Suggestion]:
51 51 text = _get_query(document)
52 52
53 53 if text.strip():
54 54 for suggestion, _ in self._find_next_match(
55 55 text, self.skip_lines, buffer.history
56 56 ):
57 57 return Suggestion(suggestion)
58 58
59 59 return None
60 60
61 61 def _find_match(
62 62 self, text: str, skip_lines: float, history: History, previous: bool
63 63 ) -> Generator[Tuple[str, float], None, None]:
64 64 """
65 65 text: str
66 66
67 67 skip_lines: float
68 68 float is used as the base value is +inf
69 69
70 70 Yields
71 71 ------
72 72 Tuple with:
73 73 str:
74 74 current suggestion.
75 75 float:
76 76 will actually yield only ints, which is passed back via skip_lines,
77 77 which may be a +inf (float)
78 78
79 79
80 80 """
81 81 line_number = -1
82 82 for string in reversed(list(history.get_strings())):
83 83 for line in reversed(string.splitlines()):
84 84 line_number += 1
85 85 if not previous and line_number < skip_lines:
86 86 continue
87 87 # do not return empty suggestions as these
88 88 # close the auto-suggestion overlay (and are useless)
89 89 if line.startswith(text) and len(line) > len(text):
90 90 yield line[len(text) :], line_number
91 91 if previous and line_number >= skip_lines:
92 92 return
93 93
94 94 def _find_next_match(self, text: str, skip_lines: float, history: History):
95 95 return self._find_match(text, skip_lines, history, previous=False)
96 96
97 97 def _find_previous_match(self, text: str, skip_lines: float, history: History):
98 98 return reversed(
99 99 list(self._find_match(text, skip_lines, history, previous=True))
100 100 )
101 101
102 102 def up(self, query: str, other_than: str, history: History):
103 103 for suggestion, line_number in self._find_next_match(
104 104 query, self.skip_lines, history
105 105 ):
106 106 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
107 107 # we want to switch from 'very.b' to 'very.a' because a) if the
108 108 # suggestion equals current text, prompt-toolkit aborts suggesting
109 109 # b) user likely would not be interested in 'very' anyways (they
110 110 # already typed it).
111 111 if query + suggestion != other_than:
112 112 self.skip_lines = line_number
113 113 break
114 114 else:
115 115 # no matches found, cycle back to beginning
116 116 self.skip_lines = 0
117 117
118 118 def down(self, query: str, other_than: str, history: History):
119 119 for suggestion, line_number in self._find_previous_match(
120 120 query, self.skip_lines, history
121 121 ):
122 122 if query + suggestion != other_than:
123 123 self.skip_lines = line_number
124 124 break
125 125 else:
126 126 # no matches found, cycle to end
127 127 for suggestion, line_number in self._find_previous_match(
128 128 query, float("Inf"), history
129 129 ):
130 130 if query + suggestion != other_than:
131 131 self.skip_lines = line_number
132 132 break
133 133
134 134
135 135 # Needed for to accept autosuggestions in vi insert mode
136 136 def accept_in_vi_insert_mode(event: KeyPressEvent):
137 137 """Apply autosuggestion if at end of line."""
138 138 buffer = event.current_buffer
139 139 d = buffer.document
140 140 after_cursor = d.text[d.cursor_position :]
141 141 lines = after_cursor.split("\n")
142 142 end_of_current_line = lines[0].strip()
143 143 suggestion = buffer.suggestion
144 144 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
145 145 buffer.insert_text(suggestion.text)
146 146 else:
147 147 nc.end_of_line(event)
148 148
149 149
150 150 def accept(event: KeyPressEvent):
151 151 """Accept autosuggestion"""
152 152 buffer = event.current_buffer
153 153 suggestion = buffer.suggestion
154 154 if suggestion:
155 155 buffer.insert_text(suggestion.text)
156 156 else:
157 157 nc.forward_char(event)
158 158
159 159
160 160 def accept_word(event: KeyPressEvent):
161 161 """Fill partial autosuggestion by word"""
162 162 buffer = event.current_buffer
163 163 suggestion = buffer.suggestion
164 164 if suggestion:
165 165 t = re.split(r"(\S+\s+)", suggestion.text)
166 166 buffer.insert_text(next((x for x in t if x), ""))
167 167 else:
168 168 nc.forward_word(event)
169 169
170 170
171 171 def accept_character(event: KeyPressEvent):
172 172 """Fill partial autosuggestion by character"""
173 173 b = event.current_buffer
174 174 suggestion = b.suggestion
175 175 if suggestion and suggestion.text:
176 176 b.insert_text(suggestion.text[0])
177 177
178 178
179 179 def accept_and_keep_cursor(event: KeyPressEvent):
180 180 """Accept autosuggestion and keep cursor in place"""
181 181 buffer = event.current_buffer
182 182 old_position = buffer.cursor_position
183 183 suggestion = buffer.suggestion
184 184 if suggestion:
185 185 buffer.insert_text(suggestion.text)
186 186 buffer.cursor_position = old_position
187 187
188 188
189 189 def accept_and_move_cursor_left(event: KeyPressEvent):
190 190 """Accept autosuggestion and move cursor left in place"""
191 191 accept_and_keep_cursor(event)
192 192 nc.backward_char(event)
193 193
194 194
195 195 def backspace_and_resume_hint(event: KeyPressEvent):
196 196 """Resume autosuggestions after deleting last character"""
197 197 current_buffer = event.current_buffer
198 198
199 199 def resume_hinting(buffer: Buffer):
200 200 if buffer.auto_suggest:
201 201 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
202 202 if suggestion:
203 203 buffer.suggestion = suggestion
204 204 current_buffer.on_text_changed.remove_handler(resume_hinting)
205 205
206 206 current_buffer.on_text_changed.add_handler(resume_hinting)
207 207 nc.backward_delete_char(event)
208 208
209 209
210 210 def accept_token(event: KeyPressEvent):
211 211 """Fill partial autosuggestion by token"""
212 212 b = event.current_buffer
213 213 suggestion = b.suggestion
214 214
215 215 if suggestion:
216 216 prefix = _get_query(b.document)
217 217 text = prefix + suggestion.text
218 218
219 219 tokens: List[Optional[str]] = [None, None, None]
220 220 substrings = [""]
221 221 i = 0
222 222
223 223 for token in generate_tokens(StringIO(text).readline):
224 224 if token.type == tokenize.NEWLINE:
225 225 index = len(text)
226 226 else:
227 227 index = text.index(token[1], len(substrings[-1]))
228 228 substrings.append(text[:index])
229 229 tokenized_so_far = substrings[-1]
230 230 if tokenized_so_far.startswith(prefix):
231 231 if i == 0 and len(tokenized_so_far) > len(prefix):
232 232 tokens[0] = tokenized_so_far[len(prefix) :]
233 233 substrings.append(tokenized_so_far)
234 234 i += 1
235 235 tokens[i] = token[1]
236 236 if i == 2:
237 237 break
238 238 i += 1
239 239
240 240 if tokens[0]:
241 241 to_insert: str
242 242 insert_text = substrings[-2]
243 243 if tokens[1] and len(tokens[1]) == 1:
244 244 insert_text = substrings[-1]
245 245 to_insert = insert_text[len(prefix) :]
246 246 b.insert_text(to_insert)
247 247 return
248 248
249 249 nc.forward_word(event)
250 250
251 251
252 252 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
253 253
254 254
255 255 def _swap_autosuggestion(
256 256 buffer: Buffer,
257 257 provider: NavigableAutoSuggestFromHistory,
258 258 direction_method: Callable,
259 259 ):
260 260 """
261 261 We skip most recent history entry (in either direction) if it equals the
262 262 current autosuggestion because if user cycles when auto-suggestion is shown
263 they most likely want something else than what was suggested (othewrise
263 they most likely want something else than what was suggested (otherwise
264 264 they would have accepted the suggestion).
265 265 """
266 266 suggestion = buffer.suggestion
267 267 if not suggestion:
268 268 return
269 269
270 270 query = _get_query(buffer.document)
271 271 current = query + suggestion.text
272 272
273 273 direction_method(query=query, other_than=current, history=buffer.history)
274 274
275 275 new_suggestion = provider.get_suggestion(buffer, buffer.document)
276 276 buffer.suggestion = new_suggestion
277 277
278 278
279 279 def swap_autosuggestion_up(provider: Provider):
280 280 def swap_autosuggestion_up(event: KeyPressEvent):
281 281 """Get next autosuggestion from history."""
282 282 if not isinstance(provider, NavigableAutoSuggestFromHistory):
283 283 return
284 284
285 285 return _swap_autosuggestion(
286 286 buffer=event.current_buffer, provider=provider, direction_method=provider.up
287 287 )
288 288
289 289 swap_autosuggestion_up.__name__ = "swap_autosuggestion_up"
290 290 return swap_autosuggestion_up
291 291
292 292
293 293 def swap_autosuggestion_down(
294 294 provider: Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
295 295 ):
296 296 def swap_autosuggestion_down(event: KeyPressEvent):
297 297 """Get previous autosuggestion from history."""
298 298 if not isinstance(provider, NavigableAutoSuggestFromHistory):
299 299 return
300 300
301 301 return _swap_autosuggestion(
302 302 buffer=event.current_buffer,
303 303 provider=provider,
304 304 direction_method=provider.down,
305 305 )
306 306
307 307 swap_autosuggestion_down.__name__ = "swap_autosuggestion_down"
308 308 return swap_autosuggestion_down
@@ -1,115 +1,115 b''
1 1 [metadata]
2 2 name = ipython
3 3 version = attr: IPython.core.release.__version__
4 4 url = https://ipython.org
5 5 description = IPython: Productive Interactive Computing
6 6 long_description_content_type = text/x-rst
7 7 long_description = file: long_description.rst
8 8 license_file = LICENSE
9 9 project_urls =
10 10 Documentation = https://ipython.readthedocs.io/
11 11 Funding = https://numfocus.org/
12 12 Source = https://github.com/ipython/ipython
13 13 Tracker = https://github.com/ipython/ipython/issues
14 14 keywords = Interactive, Interpreter, Shell, Embedding
15 15 platforms = Linux, Mac OSX, Windows
16 16 classifiers =
17 17 Framework :: IPython
18 18 Framework :: Jupyter
19 19 Intended Audience :: Developers
20 20 Intended Audience :: Science/Research
21 21 License :: OSI Approved :: BSD License
22 22 Programming Language :: Python
23 23 Programming Language :: Python :: 3
24 24 Programming Language :: Python :: 3 :: Only
25 25 Topic :: System :: Shells
26 26
27 27 [options]
28 28 packages = find:
29 29 python_requires = >=3.8
30 30 zip_safe = False
31 31 install_requires =
32 32 appnope; sys_platform == "darwin"
33 33 backcall
34 34 colorama; sys_platform == "win32"
35 35 decorator
36 36 jedi>=0.16
37 37 matplotlib-inline
38 38 pexpect>4.3; sys_platform != "win32"
39 39 pickleshare
40 prompt_toolkit>=3.0.11,<3.1.0
40 prompt_toolkit>=3.0.30,<3.1.0
41 41 pygments>=2.4.0
42 42 stack_data
43 43 traitlets>=5
44 44
45 45 [options.extras_require]
46 46 black =
47 47 black
48 48 doc =
49 49 ipykernel
50 50 setuptools>=18.5
51 51 sphinx>=1.3
52 52 sphinx-rtd-theme
53 53 docrepr
54 54 matplotlib
55 55 stack_data
56 56 pytest<7
57 57 typing_extensions
58 58 %(test)s
59 59 kernel =
60 60 ipykernel
61 61 nbconvert =
62 62 nbconvert
63 63 nbformat =
64 64 nbformat
65 65 notebook =
66 66 ipywidgets
67 67 notebook
68 68 parallel =
69 69 ipyparallel
70 70 qtconsole =
71 71 qtconsole
72 72 terminal =
73 73 test =
74 74 pytest<7.1
75 75 pytest-asyncio
76 76 testpath
77 77 test_extra =
78 78 %(test)s
79 79 curio
80 80 matplotlib!=3.2.0
81 81 nbformat
82 82 numpy>=1.20
83 83 pandas
84 84 trio
85 85 all =
86 86 %(black)s
87 87 %(doc)s
88 88 %(kernel)s
89 89 %(nbconvert)s
90 90 %(nbformat)s
91 91 %(notebook)s
92 92 %(parallel)s
93 93 %(qtconsole)s
94 94 %(terminal)s
95 95 %(test_extra)s
96 96 %(test)s
97 97
98 98 [options.packages.find]
99 99 exclude =
100 100 setupext
101 101
102 102 [options.package_data]
103 103 IPython = py.typed
104 104 IPython.core = profile/README*
105 105 IPython.core.tests = *.png, *.jpg, daft_extension/*.py
106 106 IPython.lib.tests = *.wav
107 107 IPython.testing.plugin = *.txt
108 108
109 109 [velin]
110 110 ignore_patterns =
111 111 IPython/core/tests
112 112 IPython/testing
113 113
114 114 [tool.black]
115 115 exclude = 'timing\.py'
General Comments 0
You need to be logged in to leave comments. Login now