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