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