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