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