##// END OF EJS Templates
Use module level getattr to emit deprecation
Matthias Bussonnier -
Show More
@@ -1,617 +1,617 b''
1 1 """
2 2 Module to define and register Terminal IPython shortcuts with
3 3 :mod:`prompt_toolkit`
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 import os
10 10 import signal
11 11 import sys
12 12 import warnings
13 13 from dataclasses import dataclass
14 14 from typing import Callable, Any, Optional, List
15 15
16 16 from prompt_toolkit.application.current import get_app
17 17 from prompt_toolkit.key_binding import KeyBindings
18 18 from prompt_toolkit.key_binding.key_processor import KeyPressEvent
19 19 from prompt_toolkit.key_binding.bindings import named_commands as nc
20 20 from prompt_toolkit.key_binding.bindings.completion import (
21 21 display_completions_like_readline,
22 22 )
23 23 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
24 24 from prompt_toolkit.filters import Condition
25 25
26 26 from IPython.core.getipython import get_ipython
27 27 from IPython.terminal.shortcuts import auto_match as match
28 28 from IPython.terminal.shortcuts import auto_suggest
29 29 from IPython.terminal.shortcuts.filters import filter_from_string
30 30 from IPython.utils.decorators import undoc
31 31
32 32 from prompt_toolkit.enums import DEFAULT_BUFFER
33 33
34 34 __all__ = ["create_ipython_shortcuts"]
35 35
36 36
37 37 @dataclass
38 38 class BaseBinding:
39 39 command: Callable[[KeyPressEvent], Any]
40 40 keys: List[str]
41 41
42 42
43 43 @dataclass
44 44 class RuntimeBinding(BaseBinding):
45 45 filter: Condition
46 46
47 47
48 48 @dataclass
49 49 class Binding(BaseBinding):
50 50 # while filter could be created by referencing variables directly (rather
51 51 # than created from strings), by using strings we ensure that users will
52 52 # be able to create filters in configuration (e.g. JSON) files too, which
53 53 # also benefits the documentation by enforcing human-readable filter names.
54 54 condition: Optional[str] = None
55 55
56 56 def __post_init__(self):
57 57 if self.condition:
58 58 self.filter = filter_from_string(self.condition)
59 59 else:
60 60 self.filter = None
61 61
62 62
63 63 def create_identifier(handler: Callable):
64 64 parts = handler.__module__.split(".")
65 65 name = handler.__name__
66 66 package = parts[0]
67 67 if len(parts) > 1:
68 68 final_module = parts[-1]
69 69 return f"{package}:{final_module}.{name}"
70 70 else:
71 71 return f"{package}:{name}"
72 72
73 73
74 74 AUTO_MATCH_BINDINGS = [
75 75 *[
76 76 Binding(
77 77 cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
78 78 )
79 79 for key, cmd in match.auto_match_parens.items()
80 80 ],
81 81 *[
82 82 # raw string
83 83 Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
84 84 for key, cmd in match.auto_match_parens_raw_string.items()
85 85 ],
86 86 Binding(
87 87 match.double_quote,
88 88 ['"'],
89 89 "focused_insert"
90 90 " & auto_match"
91 91 " & not_inside_unclosed_string"
92 92 " & preceded_by_paired_double_quotes"
93 93 " & followed_by_closing_paren_or_end",
94 94 ),
95 95 Binding(
96 96 match.single_quote,
97 97 ["'"],
98 98 "focused_insert"
99 99 " & auto_match"
100 100 " & not_inside_unclosed_string"
101 101 " & preceded_by_paired_single_quotes"
102 102 " & followed_by_closing_paren_or_end",
103 103 ),
104 104 Binding(
105 105 match.docstring_double_quotes,
106 106 ['"'],
107 107 "focused_insert"
108 108 " & auto_match"
109 109 " & not_inside_unclosed_string"
110 110 " & preceded_by_two_double_quotes",
111 111 ),
112 112 Binding(
113 113 match.docstring_single_quotes,
114 114 ["'"],
115 115 "focused_insert"
116 116 " & auto_match"
117 117 " & not_inside_unclosed_string"
118 118 " & preceded_by_two_single_quotes",
119 119 ),
120 120 Binding(
121 121 match.skip_over,
122 122 [")"],
123 123 "focused_insert & auto_match & followed_by_closing_round_paren",
124 124 ),
125 125 Binding(
126 126 match.skip_over,
127 127 ["]"],
128 128 "focused_insert & auto_match & followed_by_closing_bracket",
129 129 ),
130 130 Binding(
131 131 match.skip_over,
132 132 ["}"],
133 133 "focused_insert & auto_match & followed_by_closing_brace",
134 134 ),
135 135 Binding(
136 136 match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
137 137 ),
138 138 Binding(
139 139 match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
140 140 ),
141 141 Binding(
142 142 match.delete_pair,
143 143 ["backspace"],
144 144 "focused_insert"
145 145 " & preceded_by_opening_round_paren"
146 146 " & auto_match"
147 147 " & followed_by_closing_round_paren",
148 148 ),
149 149 Binding(
150 150 match.delete_pair,
151 151 ["backspace"],
152 152 "focused_insert"
153 153 " & preceded_by_opening_bracket"
154 154 " & auto_match"
155 155 " & followed_by_closing_bracket",
156 156 ),
157 157 Binding(
158 158 match.delete_pair,
159 159 ["backspace"],
160 160 "focused_insert"
161 161 " & preceded_by_opening_brace"
162 162 " & auto_match"
163 163 " & followed_by_closing_brace",
164 164 ),
165 165 Binding(
166 166 match.delete_pair,
167 167 ["backspace"],
168 168 "focused_insert"
169 169 " & preceded_by_double_quote"
170 170 " & auto_match"
171 171 " & followed_by_double_quote",
172 172 ),
173 173 Binding(
174 174 match.delete_pair,
175 175 ["backspace"],
176 176 "focused_insert"
177 177 " & preceded_by_single_quote"
178 178 " & auto_match"
179 179 " & followed_by_single_quote",
180 180 ),
181 181 ]
182 182
183 183 AUTO_SUGGEST_BINDINGS = [
184 184 # there are two reasons for re-defining bindings defined upstream:
185 185 # 1) prompt-toolkit does not execute autosuggestion bindings in vi mode,
186 186 # 2) prompt-toolkit checks if we are at the end of text, not end of line
187 187 # hence it does not work in multi-line mode of navigable provider
188 188 Binding(
189 auto_suggest.accept_in_vi_insert_mode,
189 auto_suggest.accept_or_jump_to_end,
190 190 ["end"],
191 191 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
192 192 ),
193 193 Binding(
194 auto_suggest.accept_in_vi_insert_mode,
194 auto_suggest.accept_or_jump_to_end,
195 195 ["c-e"],
196 196 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
197 197 ),
198 198 Binding(
199 199 auto_suggest.accept,
200 200 ["c-f"],
201 201 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
202 202 ),
203 203 Binding(
204 204 auto_suggest.accept,
205 205 ["right"],
206 206 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
207 207 ),
208 208 Binding(
209 209 auto_suggest.accept_word,
210 210 ["escape", "f"],
211 211 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
212 212 ),
213 213 Binding(
214 214 auto_suggest.accept_token,
215 215 ["c-right"],
216 216 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
217 217 ),
218 218 Binding(
219 219 auto_suggest.discard,
220 220 ["escape"],
221 221 # note this one is using `emacs_insert_mode`, not `emacs_like_insert_mode`
222 222 # as in `vi_insert_mode` we do not want `escape` to be shadowed (ever).
223 223 "has_suggestion & default_buffer_focused & emacs_insert_mode",
224 224 ),
225 225 Binding(
226 226 auto_suggest.swap_autosuggestion_up,
227 227 ["up"],
228 228 "navigable_suggestions"
229 229 " & ~has_line_above"
230 230 " & has_suggestion"
231 231 " & default_buffer_focused",
232 232 ),
233 233 Binding(
234 234 auto_suggest.swap_autosuggestion_down,
235 235 ["down"],
236 236 "navigable_suggestions"
237 237 " & ~has_line_below"
238 238 " & has_suggestion"
239 239 " & default_buffer_focused",
240 240 ),
241 241 Binding(
242 242 auto_suggest.up_and_update_hint,
243 243 ["up"],
244 244 "has_line_above & navigable_suggestions & default_buffer_focused",
245 245 ),
246 246 Binding(
247 247 auto_suggest.down_and_update_hint,
248 248 ["down"],
249 249 "has_line_below & navigable_suggestions & default_buffer_focused",
250 250 ),
251 251 Binding(
252 252 auto_suggest.accept_character,
253 253 ["escape", "right"],
254 254 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
255 255 ),
256 256 Binding(
257 257 auto_suggest.accept_and_move_cursor_left,
258 258 ["c-left"],
259 259 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
260 260 ),
261 261 Binding(
262 262 auto_suggest.accept_and_keep_cursor,
263 263 ["c-down"],
264 264 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
265 265 ),
266 266 Binding(
267 267 auto_suggest.backspace_and_resume_hint,
268 268 ["backspace"],
269 269 # no `has_suggestion` here to allow resuming if no suggestion
270 270 "default_buffer_focused & emacs_like_insert_mode",
271 271 ),
272 272 ]
273 273
274 274
275 275 SIMPLE_CONTROL_BINDINGS = [
276 276 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
277 277 for key, cmd in {
278 278 "c-a": nc.beginning_of_line,
279 279 "c-b": nc.backward_char,
280 280 "c-k": nc.kill_line,
281 281 "c-w": nc.backward_kill_word,
282 282 "c-y": nc.yank,
283 283 "c-_": nc.undo,
284 284 }.items()
285 285 ]
286 286
287 287
288 288 ALT_AND_COMOBO_CONTROL_BINDINGS = [
289 289 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
290 290 for keys, cmd in {
291 291 # Control Combos
292 292 ("c-x", "c-e"): nc.edit_and_execute,
293 293 ("c-x", "e"): nc.edit_and_execute,
294 294 # Alt
295 295 ("escape", "b"): nc.backward_word,
296 296 ("escape", "c"): nc.capitalize_word,
297 297 ("escape", "d"): nc.kill_word,
298 298 ("escape", "h"): nc.backward_kill_word,
299 299 ("escape", "l"): nc.downcase_word,
300 300 ("escape", "u"): nc.uppercase_word,
301 301 ("escape", "y"): nc.yank_pop,
302 302 ("escape", "."): nc.yank_last_arg,
303 303 }.items()
304 304 ]
305 305
306 306
307 307 def add_binding(bindings: KeyBindings, binding: Binding):
308 308 bindings.add(
309 309 *binding.keys,
310 310 **({"filter": binding.filter} if binding.filter is not None else {}),
311 311 )(binding.command)
312 312
313 313
314 314 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
315 315 """Set up the prompt_toolkit keyboard shortcuts for IPython.
316 316
317 317 Parameters
318 318 ----------
319 319 shell: InteractiveShell
320 320 The current IPython shell Instance
321 321 skip: List[Binding]
322 322 Bindings to skip.
323 323
324 324 Returns
325 325 -------
326 326 KeyBindings
327 327 the keybinding instance for prompt toolkit.
328 328
329 329 """
330 330 kb = KeyBindings()
331 331 skip = skip or []
332 332 for binding in KEY_BINDINGS:
333 333 skip_this_one = False
334 334 for to_skip in skip:
335 335 if (
336 336 to_skip.command == binding.command
337 337 and to_skip.filter == binding.filter
338 338 and to_skip.keys == binding.keys
339 339 ):
340 340 skip_this_one = True
341 341 break
342 342 if skip_this_one:
343 343 continue
344 344 add_binding(kb, binding)
345 345
346 346 def get_input_mode(self):
347 347 app = get_app()
348 348 app.ttimeoutlen = shell.ttimeoutlen
349 349 app.timeoutlen = shell.timeoutlen
350 350
351 351 return self._input_mode
352 352
353 353 def set_input_mode(self, mode):
354 354 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
355 355 cursor = "\x1b[{} q".format(shape)
356 356
357 357 sys.stdout.write(cursor)
358 358 sys.stdout.flush()
359 359
360 360 self._input_mode = mode
361 361
362 362 if shell.editing_mode == "vi" and shell.modal_cursor:
363 363 ViState._input_mode = InputMode.INSERT # type: ignore
364 364 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
365 365
366 366 return kb
367 367
368 368
369 369 def reformat_and_execute(event):
370 370 """Reformat code and execute it"""
371 371 shell = get_ipython()
372 372 reformat_text_before_cursor(
373 373 event.current_buffer, event.current_buffer.document, shell
374 374 )
375 375 event.current_buffer.validate_and_handle()
376 376
377 377
378 378 def reformat_text_before_cursor(buffer, document, shell):
379 379 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
380 380 try:
381 381 formatted_text = shell.reformat_handler(text)
382 382 buffer.insert_text(formatted_text)
383 383 except Exception as e:
384 384 buffer.insert_text(text)
385 385
386 386
387 387 def handle_return_or_newline_or_execute(event):
388 388 shell = get_ipython()
389 389 if getattr(shell, "handle_return", None):
390 390 return shell.handle_return(shell)(event)
391 391 else:
392 392 return newline_or_execute_outer(shell)(event)
393 393
394 394
395 395 def newline_or_execute_outer(shell):
396 396 def newline_or_execute(event):
397 397 """When the user presses return, insert a newline or execute the code."""
398 398 b = event.current_buffer
399 399 d = b.document
400 400
401 401 if b.complete_state:
402 402 cc = b.complete_state.current_completion
403 403 if cc:
404 404 b.apply_completion(cc)
405 405 else:
406 406 b.cancel_completion()
407 407 return
408 408
409 409 # If there's only one line, treat it as if the cursor is at the end.
410 410 # See https://github.com/ipython/ipython/issues/10425
411 411 if d.line_count == 1:
412 412 check_text = d.text
413 413 else:
414 414 check_text = d.text[: d.cursor_position]
415 415 status, indent = shell.check_complete(check_text)
416 416
417 417 # if all we have after the cursor is whitespace: reformat current text
418 418 # before cursor
419 419 after_cursor = d.text[d.cursor_position :]
420 420 reformatted = False
421 421 if not after_cursor.strip():
422 422 reformat_text_before_cursor(b, d, shell)
423 423 reformatted = True
424 424 if not (
425 425 d.on_last_line
426 426 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
427 427 ):
428 428 if shell.autoindent:
429 429 b.insert_text("\n" + indent)
430 430 else:
431 431 b.insert_text("\n")
432 432 return
433 433
434 434 if (status != "incomplete") and b.accept_handler:
435 435 if not reformatted:
436 436 reformat_text_before_cursor(b, d, shell)
437 437 b.validate_and_handle()
438 438 else:
439 439 if shell.autoindent:
440 440 b.insert_text("\n" + indent)
441 441 else:
442 442 b.insert_text("\n")
443 443
444 444 return newline_or_execute
445 445
446 446
447 447 def previous_history_or_previous_completion(event):
448 448 """
449 449 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
450 450
451 451 If completer is open this still select previous completion.
452 452 """
453 453 event.current_buffer.auto_up()
454 454
455 455
456 456 def next_history_or_next_completion(event):
457 457 """
458 458 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
459 459
460 460 If completer is open this still select next completion.
461 461 """
462 462 event.current_buffer.auto_down()
463 463
464 464
465 465 def dismiss_completion(event):
466 466 """Dismiss completion"""
467 467 b = event.current_buffer
468 468 if b.complete_state:
469 469 b.cancel_completion()
470 470
471 471
472 472 def reset_buffer(event):
473 473 """Reset buffer"""
474 474 b = event.current_buffer
475 475 if b.complete_state:
476 476 b.cancel_completion()
477 477 else:
478 478 b.reset()
479 479
480 480
481 481 def reset_search_buffer(event):
482 482 """Reset search buffer"""
483 483 if event.current_buffer.document.text:
484 484 event.current_buffer.reset()
485 485 else:
486 486 event.app.layout.focus(DEFAULT_BUFFER)
487 487
488 488
489 489 def suspend_to_bg(event):
490 490 """Suspend to background"""
491 491 event.app.suspend_to_background()
492 492
493 493
494 494 def quit(event):
495 495 """
496 496 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
497 497
498 498 On platforms that support SIGQUIT, send SIGQUIT to the current process.
499 499 On other platforms, just exit the process with a message.
500 500 """
501 501 sigquit = getattr(signal, "SIGQUIT", None)
502 502 if sigquit is not None:
503 503 os.kill(0, signal.SIGQUIT)
504 504 else:
505 505 sys.exit("Quit")
506 506
507 507
508 508 def indent_buffer(event):
509 509 """Indent buffer"""
510 510 event.current_buffer.insert_text(" " * 4)
511 511
512 512
513 513 def newline_autoindent(event):
514 514 """Insert a newline after the cursor indented appropriately.
515 515
516 516 Fancier version of former ``newline_with_copy_margin`` which should
517 517 compute the correct indentation of the inserted line. That is to say, indent
518 518 by 4 extra space after a function definition, class definition, context
519 519 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
520 520 """
521 521 shell = get_ipython()
522 522 inputsplitter = shell.input_transformer_manager
523 523 b = event.current_buffer
524 524 d = b.document
525 525
526 526 if b.complete_state:
527 527 b.cancel_completion()
528 528 text = d.text[: d.cursor_position] + "\n"
529 529 _, indent = inputsplitter.check_complete(text)
530 530 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
531 531
532 532
533 533 def open_input_in_editor(event):
534 534 """Open code from input in external editor"""
535 535 event.app.current_buffer.open_in_editor()
536 536
537 537
538 538 if sys.platform == "win32":
539 539 from IPython.core.error import TryNext
540 540 from IPython.lib.clipboard import (
541 541 ClipboardEmpty,
542 542 tkinter_clipboard_get,
543 543 win32_clipboard_get,
544 544 )
545 545
546 546 @undoc
547 547 def win_paste(event):
548 548 try:
549 549 text = win32_clipboard_get()
550 550 except TryNext:
551 551 try:
552 552 text = tkinter_clipboard_get()
553 553 except (TryNext, ClipboardEmpty):
554 554 return
555 555 except ClipboardEmpty:
556 556 return
557 557 event.current_buffer.insert_text(text.replace("\t", " " * 4))
558 558
559 559 else:
560 560
561 561 @undoc
562 562 def win_paste(event):
563 563 """Stub used on other platforms"""
564 564 pass
565 565
566 566
567 567 KEY_BINDINGS = [
568 568 Binding(
569 569 handle_return_or_newline_or_execute,
570 570 ["enter"],
571 571 "default_buffer_focused & ~has_selection & insert_mode",
572 572 ),
573 573 Binding(
574 574 reformat_and_execute,
575 575 ["escape", "enter"],
576 576 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
577 577 ),
578 578 Binding(quit, ["c-\\"]),
579 579 Binding(
580 580 previous_history_or_previous_completion,
581 581 ["c-p"],
582 582 "vi_insert_mode & default_buffer_focused",
583 583 ),
584 584 Binding(
585 585 next_history_or_next_completion,
586 586 ["c-n"],
587 587 "vi_insert_mode & default_buffer_focused",
588 588 ),
589 589 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
590 590 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
591 591 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
592 592 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
593 593 Binding(
594 594 indent_buffer,
595 595 ["tab"], # Ctrl+I == Tab
596 596 "default_buffer_focused"
597 597 " & ~has_selection"
598 598 " & insert_mode"
599 599 " & cursor_in_leading_ws",
600 600 ),
601 601 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
602 602 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
603 603 *AUTO_MATCH_BINDINGS,
604 604 *AUTO_SUGGEST_BINDINGS,
605 605 Binding(
606 606 display_completions_like_readline,
607 607 ["c-i"],
608 608 "readline_like_completions"
609 609 " & default_buffer_focused"
610 610 " & ~has_selection"
611 611 " & insert_mode"
612 612 " & ~cursor_in_leading_ws",
613 613 ),
614 614 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
615 615 *SIMPLE_CONTROL_BINDINGS,
616 616 *ALT_AND_COMOBO_CONTROL_BINDINGS,
617 617 ]
@@ -1,383 +1,397 b''
1 1 import re
2 2 import tokenize
3 3 from io import StringIO
4 4 from typing import Callable, List, Optional, Union, Generator, Tuple
5 import warnings
5 6
6 7 from prompt_toolkit.buffer import Buffer
7 8 from prompt_toolkit.key_binding import KeyPressEvent
8 9 from prompt_toolkit.key_binding.bindings import named_commands as nc
9 10 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
10 11 from prompt_toolkit.document import Document
11 12 from prompt_toolkit.history import History
12 13 from prompt_toolkit.shortcuts import PromptSession
13 14 from prompt_toolkit.layout.processors import (
14 15 Processor,
15 16 Transformation,
16 17 TransformationInput,
17 18 )
18 19
19 20 from IPython.core.getipython import get_ipython
20 21 from IPython.utils.tokenutil import generate_tokens
21 22
22 23
23 24 def _get_query(document: Document):
24 25 return document.lines[document.cursor_position_row]
25 26
26 27
27 28 class AppendAutoSuggestionInAnyLine(Processor):
28 29 """
29 30 Append the auto suggestion to lines other than the last (appending to the
30 31 last line is natively supported by the prompt toolkit).
31 32 """
32 33
33 34 def __init__(self, style: str = "class:auto-suggestion") -> None:
34 35 self.style = style
35 36
36 37 def apply_transformation(self, ti: TransformationInput) -> Transformation:
37 38 is_last_line = ti.lineno == ti.document.line_count - 1
38 39 is_active_line = ti.lineno == ti.document.cursor_position_row
39 40
40 41 if not is_last_line and is_active_line:
41 42 buffer = ti.buffer_control.buffer
42 43
43 44 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
44 45 suggestion = buffer.suggestion.text
45 46 else:
46 47 suggestion = ""
47 48
48 49 return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
49 50 else:
50 51 return Transformation(fragments=ti.fragments)
51 52
52 53
53 54 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
54 55 """
55 56 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
56 57 suggestion from history. To do so it remembers the current position, but it
57 58 state need to carefully be cleared on the right events.
58 59 """
59 60
60 61 def __init__(
61 62 self,
62 63 ):
63 64 self.skip_lines = 0
64 65 self._connected_apps = []
65 66
66 67 def reset_history_position(self, _: Buffer):
67 68 self.skip_lines = 0
68 69
69 70 def disconnect(self):
70 71 for pt_app in self._connected_apps:
71 72 text_insert_event = pt_app.default_buffer.on_text_insert
72 73 text_insert_event.remove_handler(self.reset_history_position)
73 74
74 75 def connect(self, pt_app: PromptSession):
75 76 self._connected_apps.append(pt_app)
76 77 # note: `on_text_changed` could be used for a bit different behaviour
77 78 # on character deletion (i.e. reseting history position on backspace)
78 79 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
79 80 pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
80 81
81 82 def get_suggestion(
82 83 self, buffer: Buffer, document: Document
83 84 ) -> Optional[Suggestion]:
84 85 text = _get_query(document)
85 86
86 87 if text.strip():
87 88 for suggestion, _ in self._find_next_match(
88 89 text, self.skip_lines, buffer.history
89 90 ):
90 91 return Suggestion(suggestion)
91 92
92 93 return None
93 94
94 95 def _dismiss(self, buffer, *args, **kwargs):
95 96 buffer.suggestion = None
96 97
97 98 def _find_match(
98 99 self, text: str, skip_lines: float, history: History, previous: bool
99 100 ) -> Generator[Tuple[str, float], None, None]:
100 101 """
101 102 text : str
102 103 Text content to find a match for, the user cursor is most of the
103 104 time at the end of this text.
104 105 skip_lines : float
105 106 number of items to skip in the search, this is used to indicate how
106 107 far in the list the user has navigated by pressing up or down.
107 108 The float type is used as the base value is +inf
108 109 history : History
109 110 prompt_toolkit History instance to fetch previous entries from.
110 111 previous : bool
111 112 Direction of the search, whether we are looking previous match
112 113 (True), or next match (False).
113 114
114 115 Yields
115 116 ------
116 117 Tuple with:
117 118 str:
118 119 current suggestion.
119 120 float:
120 121 will actually yield only ints, which is passed back via skip_lines,
121 122 which may be a +inf (float)
122 123
123 124
124 125 """
125 126 line_number = -1
126 127 for string in reversed(list(history.get_strings())):
127 128 for line in reversed(string.splitlines()):
128 129 line_number += 1
129 130 if not previous and line_number < skip_lines:
130 131 continue
131 132 # do not return empty suggestions as these
132 133 # close the auto-suggestion overlay (and are useless)
133 134 if line.startswith(text) and len(line) > len(text):
134 135 yield line[len(text) :], line_number
135 136 if previous and line_number >= skip_lines:
136 137 return
137 138
138 139 def _find_next_match(
139 140 self, text: str, skip_lines: float, history: History
140 141 ) -> Generator[Tuple[str, float], None, None]:
141 142 return self._find_match(text, skip_lines, history, previous=False)
142 143
143 144 def _find_previous_match(self, text: str, skip_lines: float, history: History):
144 145 return reversed(
145 146 list(self._find_match(text, skip_lines, history, previous=True))
146 147 )
147 148
148 149 def up(self, query: str, other_than: str, history: History) -> None:
149 150 for suggestion, line_number in self._find_next_match(
150 151 query, self.skip_lines, history
151 152 ):
152 153 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
153 154 # we want to switch from 'very.b' to 'very.a' because a) if the
154 155 # suggestion equals current text, prompt-toolkit aborts suggesting
155 156 # b) user likely would not be interested in 'very' anyways (they
156 157 # already typed it).
157 158 if query + suggestion != other_than:
158 159 self.skip_lines = line_number
159 160 break
160 161 else:
161 162 # no matches found, cycle back to beginning
162 163 self.skip_lines = 0
163 164
164 165 def down(self, query: str, other_than: str, history: History) -> None:
165 166 for suggestion, line_number in self._find_previous_match(
166 167 query, self.skip_lines, history
167 168 ):
168 169 if query + suggestion != other_than:
169 170 self.skip_lines = line_number
170 171 break
171 172 else:
172 173 # no matches found, cycle to end
173 174 for suggestion, line_number in self._find_previous_match(
174 175 query, float("Inf"), history
175 176 ):
176 177 if query + suggestion != other_than:
177 178 self.skip_lines = line_number
178 179 break
179 180
180 181
181 182 def accept_or_jump_to_end(event: KeyPressEvent):
182 183 """Apply autosuggestion or jump to end of line."""
183 184 buffer = event.current_buffer
184 185 d = buffer.document
185 186 after_cursor = d.text[d.cursor_position :]
186 187 lines = after_cursor.split("\n")
187 188 end_of_current_line = lines[0].strip()
188 189 suggestion = buffer.suggestion
189 190 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
190 191 buffer.insert_text(suggestion.text)
191 192 else:
192 193 nc.end_of_line(event)
193 194
194 195
195 def accept_in_vi_insert_mode(event: KeyPressEvent):
196 def _deprected_accept_in_vi_insert_mode(event: KeyPressEvent):
196 197 """Accept autosuggestion or jump to end of line.
197 198
198 199 .. deprecated:: 8.12
199 200 Use `accept_or_jump_to_end` instead.
200 201 """
201 202 return accept_or_jump_to_end(event)
202 203
203 204
204 205 def accept(event: KeyPressEvent):
205 206 """Accept autosuggestion"""
206 207 buffer = event.current_buffer
207 208 suggestion = buffer.suggestion
208 209 if suggestion:
209 210 buffer.insert_text(suggestion.text)
210 211 else:
211 212 nc.forward_char(event)
212 213
213 214
214 215 def discard(event: KeyPressEvent):
215 216 """Discard autosuggestion"""
216 217 buffer = event.current_buffer
217 218 buffer.suggestion = None
218 219
219 220
220 221 def accept_word(event: KeyPressEvent):
221 222 """Fill partial autosuggestion by word"""
222 223 buffer = event.current_buffer
223 224 suggestion = buffer.suggestion
224 225 if suggestion:
225 226 t = re.split(r"(\S+\s+)", suggestion.text)
226 227 buffer.insert_text(next((x for x in t if x), ""))
227 228 else:
228 229 nc.forward_word(event)
229 230
230 231
231 232 def accept_character(event: KeyPressEvent):
232 233 """Fill partial autosuggestion by character"""
233 234 b = event.current_buffer
234 235 suggestion = b.suggestion
235 236 if suggestion and suggestion.text:
236 237 b.insert_text(suggestion.text[0])
237 238
238 239
239 240 def accept_and_keep_cursor(event: KeyPressEvent):
240 241 """Accept autosuggestion and keep cursor in place"""
241 242 buffer = event.current_buffer
242 243 old_position = buffer.cursor_position
243 244 suggestion = buffer.suggestion
244 245 if suggestion:
245 246 buffer.insert_text(suggestion.text)
246 247 buffer.cursor_position = old_position
247 248
248 249
249 250 def accept_and_move_cursor_left(event: KeyPressEvent):
250 251 """Accept autosuggestion and move cursor left in place"""
251 252 accept_and_keep_cursor(event)
252 253 nc.backward_char(event)
253 254
254 255
255 256 def _update_hint(buffer: Buffer):
256 257 if buffer.auto_suggest:
257 258 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
258 259 buffer.suggestion = suggestion
259 260
260 261
261 262 def backspace_and_resume_hint(event: KeyPressEvent):
262 263 """Resume autosuggestions after deleting last character"""
263 264 current_buffer = event.current_buffer
264 265
265 266 def resume_hinting(buffer: Buffer):
266 267 _update_hint(buffer)
267 268 current_buffer.on_text_changed.remove_handler(resume_hinting)
268 269
269 270 current_buffer.on_text_changed.add_handler(resume_hinting)
270 271 nc.backward_delete_char(event)
271 272
272 273
273 274 def up_and_update_hint(event: KeyPressEvent):
274 275 """Go up and update hint"""
275 276 current_buffer = event.current_buffer
276 277
277 278 current_buffer.auto_up(count=event.arg)
278 279 _update_hint(current_buffer)
279 280
280 281
281 282 def down_and_update_hint(event: KeyPressEvent):
282 283 """Go down and update hint"""
283 284 current_buffer = event.current_buffer
284 285
285 286 current_buffer.auto_down(count=event.arg)
286 287 _update_hint(current_buffer)
287 288
288 289
289 290 def accept_token(event: KeyPressEvent):
290 291 """Fill partial autosuggestion by token"""
291 292 b = event.current_buffer
292 293 suggestion = b.suggestion
293 294
294 295 if suggestion:
295 296 prefix = _get_query(b.document)
296 297 text = prefix + suggestion.text
297 298
298 299 tokens: List[Optional[str]] = [None, None, None]
299 300 substrings = [""]
300 301 i = 0
301 302
302 303 for token in generate_tokens(StringIO(text).readline):
303 304 if token.type == tokenize.NEWLINE:
304 305 index = len(text)
305 306 else:
306 307 index = text.index(token[1], len(substrings[-1]))
307 308 substrings.append(text[:index])
308 309 tokenized_so_far = substrings[-1]
309 310 if tokenized_so_far.startswith(prefix):
310 311 if i == 0 and len(tokenized_so_far) > len(prefix):
311 312 tokens[0] = tokenized_so_far[len(prefix) :]
312 313 substrings.append(tokenized_so_far)
313 314 i += 1
314 315 tokens[i] = token[1]
315 316 if i == 2:
316 317 break
317 318 i += 1
318 319
319 320 if tokens[0]:
320 321 to_insert: str
321 322 insert_text = substrings[-2]
322 323 if tokens[1] and len(tokens[1]) == 1:
323 324 insert_text = substrings[-1]
324 325 to_insert = insert_text[len(prefix) :]
325 326 b.insert_text(to_insert)
326 327 return
327 328
328 329 nc.forward_word(event)
329 330
330 331
331 332 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
332 333
333 334
334 335 def _swap_autosuggestion(
335 336 buffer: Buffer,
336 337 provider: NavigableAutoSuggestFromHistory,
337 338 direction_method: Callable,
338 339 ):
339 340 """
340 341 We skip most recent history entry (in either direction) if it equals the
341 342 current autosuggestion because if user cycles when auto-suggestion is shown
342 343 they most likely want something else than what was suggested (otherwise
343 344 they would have accepted the suggestion).
344 345 """
345 346 suggestion = buffer.suggestion
346 347 if not suggestion:
347 348 return
348 349
349 350 query = _get_query(buffer.document)
350 351 current = query + suggestion.text
351 352
352 353 direction_method(query=query, other_than=current, history=buffer.history)
353 354
354 355 new_suggestion = provider.get_suggestion(buffer, buffer.document)
355 356 buffer.suggestion = new_suggestion
356 357
357 358
358 359 def swap_autosuggestion_up(event: KeyPressEvent):
359 360 """Get next autosuggestion from history."""
360 361 shell = get_ipython()
361 362 provider = shell.auto_suggest
362 363
363 364 if not isinstance(provider, NavigableAutoSuggestFromHistory):
364 365 return
365 366
366 367 return _swap_autosuggestion(
367 368 buffer=event.current_buffer, provider=provider, direction_method=provider.up
368 369 )
369 370
370 371
371 372 def swap_autosuggestion_down(event: KeyPressEvent):
372 373 """Get previous autosuggestion from history."""
373 374 shell = get_ipython()
374 375 provider = shell.auto_suggest
375 376
376 377 if not isinstance(provider, NavigableAutoSuggestFromHistory):
377 378 return
378 379
379 380 return _swap_autosuggestion(
380 381 buffer=event.current_buffer,
381 382 provider=provider,
382 383 direction_method=provider.down,
383 384 )
385
386
387 def __getattr__(key):
388 if key == "accept_in_vi_insert_mode":
389 warnings.warn(
390 "`accept_in_vi_insert_mode` is deprecated since IPython 8.12 and "
391 "renamed to `accept_or_jump_to_end`. Please update your configuration "
392 "accordingly",
393 DeprecationWarning,
394 stacklevel=2,
395 )
396 return _deprected_accept_in_vi_insert_mode
397 raise AttributeError
@@ -1,461 +1,468 b''
1 1 import pytest
2 2 from IPython.terminal.shortcuts.auto_suggest import (
3 3 accept,
4 accept_in_vi_insert_mode,
4 accept_or_jump_to_end,
5 5 accept_token,
6 6 accept_character,
7 7 accept_word,
8 8 accept_and_keep_cursor,
9 9 discard,
10 10 NavigableAutoSuggestFromHistory,
11 11 swap_autosuggestion_up,
12 12 swap_autosuggestion_down,
13 13 )
14 14 from IPython.terminal.shortcuts.auto_match import skip_over
15 15 from IPython.terminal.shortcuts import create_ipython_shortcuts
16 16
17 17 from prompt_toolkit.history import InMemoryHistory
18 18 from prompt_toolkit.buffer import Buffer
19 19 from prompt_toolkit.document import Document
20 20 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
21 21
22 22 from unittest.mock import patch, Mock
23 23
24 24
25 def test_deprected():
26 import IPython.terminal.shortcuts.auto_suggest as iptsa
27
28 with pytest.warns(DeprecationWarning, match=r"8\.12.+accept_or_jump_to_end"):
29 iptsa.accept_in_vi_insert_mode
30
31
25 32 def make_event(text, cursor, suggestion):
26 33 event = Mock()
27 34 event.current_buffer = Mock()
28 35 event.current_buffer.suggestion = Mock()
29 36 event.current_buffer.text = text
30 37 event.current_buffer.cursor_position = cursor
31 38 event.current_buffer.suggestion.text = suggestion
32 39 event.current_buffer.document = Document(text=text, cursor_position=cursor)
33 40 return event
34 41
35 42
36 43 @pytest.mark.parametrize(
37 44 "text, suggestion, expected",
38 45 [
39 46 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):"),
40 47 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):"),
41 48 ],
42 49 )
43 50 def test_accept(text, suggestion, expected):
44 51 event = make_event(text, len(text), suggestion)
45 52 buffer = event.current_buffer
46 53 buffer.insert_text = Mock()
47 54 accept(event)
48 55 assert buffer.insert_text.called
49 56 assert buffer.insert_text.call_args[0] == (expected,)
50 57
51 58
52 59 @pytest.mark.parametrize(
53 60 "text, suggestion",
54 61 [
55 62 ("", "def out(tag: str, n=50):"),
56 63 ("def ", "out(tag: str, n=50):"),
57 64 ],
58 65 )
59 66 def test_discard(text, suggestion):
60 67 event = make_event(text, len(text), suggestion)
61 68 buffer = event.current_buffer
62 69 buffer.insert_text = Mock()
63 70 discard(event)
64 71 assert not buffer.insert_text.called
65 72 assert buffer.suggestion is None
66 73
67 74
68 75 @pytest.mark.parametrize(
69 76 "text, cursor, suggestion, called",
70 77 [
71 78 ("123456", 6, "123456789", True),
72 79 ("123456", 3, "123456789", False),
73 80 ("123456 \n789", 6, "123456789", True),
74 81 ],
75 82 )
76 83 def test_autosuggest_at_EOL(text, cursor, suggestion, called):
77 84 """
78 85 test that autosuggest is only applied at end of line.
79 86 """
80 87
81 88 event = make_event(text, cursor, suggestion)
82 89 event.current_buffer.insert_text = Mock()
83 accept_in_vi_insert_mode(event)
90 accept_or_jump_to_end(event)
84 91 if called:
85 92 event.current_buffer.insert_text.assert_called()
86 93 else:
87 94 event.current_buffer.insert_text.assert_not_called()
88 95 # event.current_buffer.document.get_end_of_line_position.assert_called()
89 96
90 97
91 98 @pytest.mark.parametrize(
92 99 "text, suggestion, expected",
93 100 [
94 101 ("", "def out(tag: str, n=50):", "def "),
95 102 ("d", "ef out(tag: str, n=50):", "ef "),
96 103 ("de ", "f out(tag: str, n=50):", "f "),
97 104 ("def", " out(tag: str, n=50):", " "),
98 105 ("def ", "out(tag: str, n=50):", "out("),
99 106 ("def o", "ut(tag: str, n=50):", "ut("),
100 107 ("def ou", "t(tag: str, n=50):", "t("),
101 108 ("def out", "(tag: str, n=50):", "("),
102 109 ("def out(", "tag: str, n=50):", "tag: "),
103 110 ("def out(t", "ag: str, n=50):", "ag: "),
104 111 ("def out(ta", "g: str, n=50):", "g: "),
105 112 ("def out(tag", ": str, n=50):", ": "),
106 113 ("def out(tag:", " str, n=50):", " "),
107 114 ("def out(tag: ", "str, n=50):", "str, "),
108 115 ("def out(tag: s", "tr, n=50):", "tr, "),
109 116 ("def out(tag: st", "r, n=50):", "r, "),
110 117 ("def out(tag: str", ", n=50):", ", n"),
111 118 ("def out(tag: str,", " n=50):", " n"),
112 119 ("def out(tag: str, ", "n=50):", "n="),
113 120 ("def out(tag: str, n", "=50):", "="),
114 121 ("def out(tag: str, n=", "50):", "50)"),
115 122 ("def out(tag: str, n=5", "0):", "0)"),
116 123 ("def out(tag: str, n=50", "):", "):"),
117 124 ("def out(tag: str, n=50)", ":", ":"),
118 125 ],
119 126 )
120 127 def test_autosuggest_token(text, suggestion, expected):
121 128 event = make_event(text, len(text), suggestion)
122 129 event.current_buffer.insert_text = Mock()
123 130 accept_token(event)
124 131 assert event.current_buffer.insert_text.called
125 132 assert event.current_buffer.insert_text.call_args[0] == (expected,)
126 133
127 134
128 135 @pytest.mark.parametrize(
129 136 "text, suggestion, expected",
130 137 [
131 138 ("", "def out(tag: str, n=50):", "d"),
132 139 ("d", "ef out(tag: str, n=50):", "e"),
133 140 ("de ", "f out(tag: str, n=50):", "f"),
134 141 ("def", " out(tag: str, n=50):", " "),
135 142 ],
136 143 )
137 144 def test_accept_character(text, suggestion, expected):
138 145 event = make_event(text, len(text), suggestion)
139 146 event.current_buffer.insert_text = Mock()
140 147 accept_character(event)
141 148 assert event.current_buffer.insert_text.called
142 149 assert event.current_buffer.insert_text.call_args[0] == (expected,)
143 150
144 151
145 152 @pytest.mark.parametrize(
146 153 "text, suggestion, expected",
147 154 [
148 155 ("", "def out(tag: str, n=50):", "def "),
149 156 ("d", "ef out(tag: str, n=50):", "ef "),
150 157 ("de", "f out(tag: str, n=50):", "f "),
151 158 ("def", " out(tag: str, n=50):", " "),
152 159 # (this is why we also have accept_token)
153 160 ("def ", "out(tag: str, n=50):", "out(tag: "),
154 161 ],
155 162 )
156 163 def test_accept_word(text, suggestion, expected):
157 164 event = make_event(text, len(text), suggestion)
158 165 event.current_buffer.insert_text = Mock()
159 166 accept_word(event)
160 167 assert event.current_buffer.insert_text.called
161 168 assert event.current_buffer.insert_text.call_args[0] == (expected,)
162 169
163 170
164 171 @pytest.mark.parametrize(
165 172 "text, suggestion, expected, cursor",
166 173 [
167 174 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):", 0),
168 175 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):", 4),
169 176 ],
170 177 )
171 178 def test_accept_and_keep_cursor(text, suggestion, expected, cursor):
172 179 event = make_event(text, cursor, suggestion)
173 180 buffer = event.current_buffer
174 181 buffer.insert_text = Mock()
175 182 accept_and_keep_cursor(event)
176 183 assert buffer.insert_text.called
177 184 assert buffer.insert_text.call_args[0] == (expected,)
178 185 assert buffer.cursor_position == cursor
179 186
180 187
181 188 def test_autosuggest_token_empty():
182 189 full = "def out(tag: str, n=50):"
183 190 event = make_event(full, len(full), "")
184 191 event.current_buffer.insert_text = Mock()
185 192
186 193 with patch(
187 194 "prompt_toolkit.key_binding.bindings.named_commands.forward_word"
188 195 ) as forward_word:
189 196 accept_token(event)
190 197 assert not event.current_buffer.insert_text.called
191 198 assert forward_word.called
192 199
193 200
194 201 def test_other_providers():
195 202 """Ensure that swapping autosuggestions does not break with other providers"""
196 203 provider = AutoSuggestFromHistory()
197 204 ip = get_ipython()
198 205 ip.auto_suggest = provider
199 206 event = Mock()
200 207 event.current_buffer = Buffer()
201 208 assert swap_autosuggestion_up(event) is None
202 209 assert swap_autosuggestion_down(event) is None
203 210
204 211
205 212 async def test_navigable_provider():
206 213 provider = NavigableAutoSuggestFromHistory()
207 214 history = InMemoryHistory(history_strings=["very_a", "very", "very_b", "very_c"])
208 215 buffer = Buffer(history=history)
209 216 ip = get_ipython()
210 217 ip.auto_suggest = provider
211 218
212 219 async for _ in history.load():
213 220 pass
214 221
215 222 buffer.cursor_position = 5
216 223 buffer.text = "very"
217 224
218 225 up = swap_autosuggestion_up
219 226 down = swap_autosuggestion_down
220 227
221 228 event = Mock()
222 229 event.current_buffer = buffer
223 230
224 231 def get_suggestion():
225 232 suggestion = provider.get_suggestion(buffer, buffer.document)
226 233 buffer.suggestion = suggestion
227 234 return suggestion
228 235
229 236 assert get_suggestion().text == "_c"
230 237
231 238 # should go up
232 239 up(event)
233 240 assert get_suggestion().text == "_b"
234 241
235 242 # should skip over 'very' which is identical to buffer content
236 243 up(event)
237 244 assert get_suggestion().text == "_a"
238 245
239 246 # should cycle back to beginning
240 247 up(event)
241 248 assert get_suggestion().text == "_c"
242 249
243 250 # should cycle back through end boundary
244 251 down(event)
245 252 assert get_suggestion().text == "_a"
246 253
247 254 down(event)
248 255 assert get_suggestion().text == "_b"
249 256
250 257 down(event)
251 258 assert get_suggestion().text == "_c"
252 259
253 260 down(event)
254 261 assert get_suggestion().text == "_a"
255 262
256 263
257 264 async def test_navigable_provider_multiline_entries():
258 265 provider = NavigableAutoSuggestFromHistory()
259 266 history = InMemoryHistory(history_strings=["very_a\nvery_b", "very_c"])
260 267 buffer = Buffer(history=history)
261 268 ip = get_ipython()
262 269 ip.auto_suggest = provider
263 270
264 271 async for _ in history.load():
265 272 pass
266 273
267 274 buffer.cursor_position = 5
268 275 buffer.text = "very"
269 276 up = swap_autosuggestion_up
270 277 down = swap_autosuggestion_down
271 278
272 279 event = Mock()
273 280 event.current_buffer = buffer
274 281
275 282 def get_suggestion():
276 283 suggestion = provider.get_suggestion(buffer, buffer.document)
277 284 buffer.suggestion = suggestion
278 285 return suggestion
279 286
280 287 assert get_suggestion().text == "_c"
281 288
282 289 up(event)
283 290 assert get_suggestion().text == "_b"
284 291
285 292 up(event)
286 293 assert get_suggestion().text == "_a"
287 294
288 295 down(event)
289 296 assert get_suggestion().text == "_b"
290 297
291 298 down(event)
292 299 assert get_suggestion().text == "_c"
293 300
294 301
295 302 def create_session_mock():
296 303 session = Mock()
297 304 session.default_buffer = Buffer()
298 305 return session
299 306
300 307
301 308 def test_navigable_provider_connection():
302 309 provider = NavigableAutoSuggestFromHistory()
303 310 provider.skip_lines = 1
304 311
305 312 session_1 = create_session_mock()
306 313 provider.connect(session_1)
307 314
308 315 assert provider.skip_lines == 1
309 316 session_1.default_buffer.on_text_insert.fire()
310 317 assert provider.skip_lines == 0
311 318
312 319 session_2 = create_session_mock()
313 320 provider.connect(session_2)
314 321 provider.skip_lines = 2
315 322
316 323 assert provider.skip_lines == 2
317 324 session_2.default_buffer.on_text_insert.fire()
318 325 assert provider.skip_lines == 0
319 326
320 327 provider.skip_lines = 3
321 328 provider.disconnect()
322 329 session_1.default_buffer.on_text_insert.fire()
323 330 session_2.default_buffer.on_text_insert.fire()
324 331 assert provider.skip_lines == 3
325 332
326 333
327 334 @pytest.fixture
328 335 def ipython_with_prompt():
329 336 ip = get_ipython()
330 337 ip.pt_app = Mock()
331 338 ip.pt_app.key_bindings = create_ipython_shortcuts(ip)
332 339 try:
333 340 yield ip
334 341 finally:
335 342 ip.pt_app = None
336 343
337 344
338 345 def find_bindings_by_command(command):
339 346 ip = get_ipython()
340 347 return [
341 348 binding
342 349 for binding in ip.pt_app.key_bindings.bindings
343 350 if binding.handler == command
344 351 ]
345 352
346 353
347 354 def test_modify_unique_shortcut(ipython_with_prompt):
348 355 original = find_bindings_by_command(accept_token)
349 356 assert len(original) == 1
350 357
351 358 ipython_with_prompt.shortcuts = [
352 359 {"command": "IPython:auto_suggest.accept_token", "new_keys": ["a", "b", "c"]}
353 360 ]
354 361 matched = find_bindings_by_command(accept_token)
355 362 assert len(matched) == 1
356 363 assert list(matched[0].keys) == ["a", "b", "c"]
357 364 assert list(matched[0].keys) != list(original[0].keys)
358 365 assert matched[0].filter == original[0].filter
359 366
360 367 ipython_with_prompt.shortcuts = [
361 368 {"command": "IPython:auto_suggest.accept_token", "new_filter": "always"}
362 369 ]
363 370 matched = find_bindings_by_command(accept_token)
364 371 assert len(matched) == 1
365 372 assert list(matched[0].keys) != ["a", "b", "c"]
366 373 assert list(matched[0].keys) == list(original[0].keys)
367 374 assert matched[0].filter != original[0].filter
368 375
369 376
370 377 def test_disable_shortcut(ipython_with_prompt):
371 378 matched = find_bindings_by_command(accept_token)
372 379 assert len(matched) == 1
373 380
374 381 ipython_with_prompt.shortcuts = [
375 382 {"command": "IPython:auto_suggest.accept_token", "new_keys": []}
376 383 ]
377 384 matched = find_bindings_by_command(accept_token)
378 385 assert len(matched) == 0
379 386
380 387 ipython_with_prompt.shortcuts = []
381 388 matched = find_bindings_by_command(accept_token)
382 389 assert len(matched) == 1
383 390
384 391
385 392 def test_modify_shortcut_with_filters(ipython_with_prompt):
386 393 matched = find_bindings_by_command(skip_over)
387 394 matched_keys = {m.keys[0] for m in matched}
388 395 assert matched_keys == {")", "]", "}", "'", '"'}
389 396
390 397 with pytest.raises(ValueError, match="Multiple shortcuts matching"):
391 398 ipython_with_prompt.shortcuts = [
392 399 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"]}
393 400 ]
394 401
395 402 ipython_with_prompt.shortcuts = [
396 403 {
397 404 "command": "IPython:auto_match.skip_over",
398 405 "new_keys": ["x"],
399 406 "match_filter": "focused_insert & auto_match & followed_by_single_quote",
400 407 }
401 408 ]
402 409 matched = find_bindings_by_command(skip_over)
403 410 matched_keys = {m.keys[0] for m in matched}
404 411 assert matched_keys == {")", "]", "}", "x", '"'}
405 412
406 413
407 414 def example_command():
408 415 pass
409 416
410 417
411 418 def test_add_shortcut_for_new_command(ipython_with_prompt):
412 419 matched = find_bindings_by_command(example_command)
413 420 assert len(matched) == 0
414 421
415 422 with pytest.raises(ValueError, match="example_command is not a known"):
416 423 ipython_with_prompt.shortcuts = [
417 424 {"command": "example_command", "new_keys": ["x"]}
418 425 ]
419 426 matched = find_bindings_by_command(example_command)
420 427 assert len(matched) == 0
421 428
422 429
423 430 def test_modify_shortcut_failure(ipython_with_prompt):
424 431 with pytest.raises(ValueError, match="No shortcuts matching"):
425 432 ipython_with_prompt.shortcuts = [
426 433 {
427 434 "command": "IPython:auto_match.skip_over",
428 435 "match_keys": ["x"],
429 436 "new_keys": ["y"],
430 437 }
431 438 ]
432 439
433 440
434 441 def test_add_shortcut_for_existing_command(ipython_with_prompt):
435 442 matched = find_bindings_by_command(skip_over)
436 443 assert len(matched) == 5
437 444
438 445 with pytest.raises(ValueError, match="Cannot add a shortcut without keys"):
439 446 ipython_with_prompt.shortcuts = [
440 447 {"command": "IPython:auto_match.skip_over", "new_keys": [], "create": True}
441 448 ]
442 449
443 450 ipython_with_prompt.shortcuts = [
444 451 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"], "create": True}
445 452 ]
446 453 matched = find_bindings_by_command(skip_over)
447 454 assert len(matched) == 6
448 455
449 456 ipython_with_prompt.shortcuts = []
450 457 matched = find_bindings_by_command(skip_over)
451 458 assert len(matched) == 5
452 459
453 460
454 461 def test_setting_shortcuts_before_pt_app_init():
455 462 ipython = get_ipython()
456 463 assert ipython.pt_app is None
457 464 shortcuts = [
458 465 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"], "create": True}
459 466 ]
460 467 ipython.shortcuts = shortcuts
461 468 assert ipython.shortcuts == shortcuts
General Comments 0
You need to be logged in to leave comments. Login now