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