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