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