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