##// END OF EJS Templates
Fix autosuggestions in multi-line mode, vi command mode delay
krassowski -
Show More
@@ -1,601 +1,616 b''
1 """
1 """
2 Module to define and register Terminal IPython shortcuts with
2 Module to define and register Terminal IPython shortcuts with
3 :mod:`prompt_toolkit`
3 :mod:`prompt_toolkit`
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 import os
9 import os
10 import signal
10 import signal
11 import sys
11 import sys
12 import warnings
12 import warnings
13 from dataclasses import dataclass
13 from dataclasses import dataclass
14 from typing import Callable, Any, Optional, List
14 from typing import Callable, Any, Optional, List
15
15
16 from prompt_toolkit.application.current import get_app
16 from prompt_toolkit.application.current import get_app
17 from prompt_toolkit.key_binding import KeyBindings
17 from prompt_toolkit.key_binding import KeyBindings
18 from prompt_toolkit.key_binding.key_processor import KeyPressEvent
18 from prompt_toolkit.key_binding.key_processor import KeyPressEvent
19 from prompt_toolkit.key_binding.bindings import named_commands as nc
19 from prompt_toolkit.key_binding.bindings import named_commands as nc
20 from prompt_toolkit.key_binding.bindings.completion import (
20 from prompt_toolkit.key_binding.bindings.completion import (
21 display_completions_like_readline,
21 display_completions_like_readline,
22 )
22 )
23 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
23 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
24 from prompt_toolkit.filters import Condition
24 from prompt_toolkit.filters import Condition
25
25
26 from IPython.core.getipython import get_ipython
26 from IPython.core.getipython import get_ipython
27 from IPython.terminal.shortcuts import auto_match as match
27 from IPython.terminal.shortcuts import auto_match as match
28 from IPython.terminal.shortcuts import auto_suggest
28 from IPython.terminal.shortcuts import auto_suggest
29 from IPython.terminal.shortcuts.filters import filter_from_string
29 from IPython.terminal.shortcuts.filters import filter_from_string
30 from IPython.utils.decorators import undoc
30 from IPython.utils.decorators import undoc
31
31
32 from prompt_toolkit.enums import DEFAULT_BUFFER
32 from prompt_toolkit.enums import DEFAULT_BUFFER
33
33
34 __all__ = ["create_ipython_shortcuts"]
34 __all__ = ["create_ipython_shortcuts"]
35
35
36
36
37 @dataclass
37 @dataclass
38 class BaseBinding:
38 class BaseBinding:
39 command: Callable[[KeyPressEvent], Any]
39 command: Callable[[KeyPressEvent], Any]
40 keys: List[str]
40 keys: List[str]
41
41
42
42
43 @dataclass
43 @dataclass
44 class RuntimeBinding(BaseBinding):
44 class RuntimeBinding(BaseBinding):
45 filter: Condition
45 filter: Condition
46
46
47
47
48 @dataclass
48 @dataclass
49 class Binding(BaseBinding):
49 class Binding(BaseBinding):
50 # while filter could be created by referencing variables directly (rather
50 # while filter could be created by referencing variables directly (rather
51 # than created from strings), by using strings we ensure that users will
51 # than created from strings), by using strings we ensure that users will
52 # be able to create filters in configuration (e.g. JSON) files too, which
52 # be able to create filters in configuration (e.g. JSON) files too, which
53 # also benefits the documentation by enforcing human-readable filter names.
53 # also benefits the documentation by enforcing human-readable filter names.
54 condition: Optional[str] = None
54 condition: Optional[str] = None
55
55
56 def __post_init__(self):
56 def __post_init__(self):
57 if self.condition:
57 if self.condition:
58 self.filter = filter_from_string(self.condition)
58 self.filter = filter_from_string(self.condition)
59 else:
59 else:
60 self.filter = None
60 self.filter = None
61
61
62
62
63 def create_identifier(handler: Callable):
63 def create_identifier(handler: Callable):
64 parts = handler.__module__.split(".")
64 parts = handler.__module__.split(".")
65 name = handler.__name__
65 name = handler.__name__
66 package = parts[0]
66 package = parts[0]
67 if len(parts) > 1:
67 if len(parts) > 1:
68 final_module = parts[-1]
68 final_module = parts[-1]
69 return f"{package}:{final_module}.{name}"
69 return f"{package}:{final_module}.{name}"
70 else:
70 else:
71 return f"{package}:{name}"
71 return f"{package}:{name}"
72
72
73
73
74 AUTO_MATCH_BINDINGS = [
74 AUTO_MATCH_BINDINGS = [
75 *[
75 *[
76 Binding(
76 Binding(
77 cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
77 cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
78 )
78 )
79 for key, cmd in match.auto_match_parens.items()
79 for key, cmd in match.auto_match_parens.items()
80 ],
80 ],
81 *[
81 *[
82 # raw string
82 # raw string
83 Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
83 Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
84 for key, cmd in match.auto_match_parens_raw_string.items()
84 for key, cmd in match.auto_match_parens_raw_string.items()
85 ],
85 ],
86 Binding(
86 Binding(
87 match.double_quote,
87 match.double_quote,
88 ['"'],
88 ['"'],
89 "focused_insert"
89 "focused_insert"
90 " & auto_match"
90 " & auto_match"
91 " & not_inside_unclosed_string"
91 " & not_inside_unclosed_string"
92 " & preceded_by_paired_double_quotes"
92 " & preceded_by_paired_double_quotes"
93 " & followed_by_closing_paren_or_end",
93 " & followed_by_closing_paren_or_end",
94 ),
94 ),
95 Binding(
95 Binding(
96 match.single_quote,
96 match.single_quote,
97 ["'"],
97 ["'"],
98 "focused_insert"
98 "focused_insert"
99 " & auto_match"
99 " & auto_match"
100 " & not_inside_unclosed_string"
100 " & not_inside_unclosed_string"
101 " & preceded_by_paired_single_quotes"
101 " & preceded_by_paired_single_quotes"
102 " & followed_by_closing_paren_or_end",
102 " & followed_by_closing_paren_or_end",
103 ),
103 ),
104 Binding(
104 Binding(
105 match.docstring_double_quotes,
105 match.docstring_double_quotes,
106 ['"'],
106 ['"'],
107 "focused_insert"
107 "focused_insert"
108 " & auto_match"
108 " & auto_match"
109 " & not_inside_unclosed_string"
109 " & not_inside_unclosed_string"
110 " & preceded_by_two_double_quotes",
110 " & preceded_by_two_double_quotes",
111 ),
111 ),
112 Binding(
112 Binding(
113 match.docstring_single_quotes,
113 match.docstring_single_quotes,
114 ["'"],
114 ["'"],
115 "focused_insert"
115 "focused_insert"
116 " & auto_match"
116 " & auto_match"
117 " & not_inside_unclosed_string"
117 " & not_inside_unclosed_string"
118 " & preceded_by_two_single_quotes",
118 " & preceded_by_two_single_quotes",
119 ),
119 ),
120 Binding(
120 Binding(
121 match.skip_over,
121 match.skip_over,
122 [")"],
122 [")"],
123 "focused_insert & auto_match & followed_by_closing_round_paren",
123 "focused_insert & auto_match & followed_by_closing_round_paren",
124 ),
124 ),
125 Binding(
125 Binding(
126 match.skip_over,
126 match.skip_over,
127 ["]"],
127 ["]"],
128 "focused_insert & auto_match & followed_by_closing_bracket",
128 "focused_insert & auto_match & followed_by_closing_bracket",
129 ),
129 ),
130 Binding(
130 Binding(
131 match.skip_over,
131 match.skip_over,
132 ["}"],
132 ["}"],
133 "focused_insert & auto_match & followed_by_closing_brace",
133 "focused_insert & auto_match & followed_by_closing_brace",
134 ),
134 ),
135 Binding(
135 Binding(
136 match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
136 match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
137 ),
137 ),
138 Binding(
138 Binding(
139 match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
139 match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
140 ),
140 ),
141 Binding(
141 Binding(
142 match.delete_pair,
142 match.delete_pair,
143 ["backspace"],
143 ["backspace"],
144 "focused_insert"
144 "focused_insert"
145 " & preceded_by_opening_round_paren"
145 " & preceded_by_opening_round_paren"
146 " & auto_match"
146 " & auto_match"
147 " & followed_by_closing_round_paren",
147 " & followed_by_closing_round_paren",
148 ),
148 ),
149 Binding(
149 Binding(
150 match.delete_pair,
150 match.delete_pair,
151 ["backspace"],
151 ["backspace"],
152 "focused_insert"
152 "focused_insert"
153 " & preceded_by_opening_bracket"
153 " & preceded_by_opening_bracket"
154 " & auto_match"
154 " & auto_match"
155 " & followed_by_closing_bracket",
155 " & followed_by_closing_bracket",
156 ),
156 ),
157 Binding(
157 Binding(
158 match.delete_pair,
158 match.delete_pair,
159 ["backspace"],
159 ["backspace"],
160 "focused_insert"
160 "focused_insert"
161 " & preceded_by_opening_brace"
161 " & preceded_by_opening_brace"
162 " & auto_match"
162 " & auto_match"
163 " & followed_by_closing_brace",
163 " & followed_by_closing_brace",
164 ),
164 ),
165 Binding(
165 Binding(
166 match.delete_pair,
166 match.delete_pair,
167 ["backspace"],
167 ["backspace"],
168 "focused_insert"
168 "focused_insert"
169 " & preceded_by_double_quote"
169 " & preceded_by_double_quote"
170 " & auto_match"
170 " & auto_match"
171 " & followed_by_double_quote",
171 " & followed_by_double_quote",
172 ),
172 ),
173 Binding(
173 Binding(
174 match.delete_pair,
174 match.delete_pair,
175 ["backspace"],
175 ["backspace"],
176 "focused_insert"
176 "focused_insert"
177 " & preceded_by_single_quote"
177 " & preceded_by_single_quote"
178 " & auto_match"
178 " & auto_match"
179 " & followed_by_single_quote",
179 " & followed_by_single_quote",
180 ),
180 ),
181 ]
181 ]
182
182
183 AUTO_SUGGEST_BINDINGS = [
183 AUTO_SUGGEST_BINDINGS = [
184 # there are two reasons for re-defining bindings defined upstream:
185 # 1) prompt-toolkit does not execute autosuggestion bindings in vi mode,
186 # 2) prompt-toolkit checks if we are at the end of text, not end of line
187 # hence it does not work in multi-line mode of navigable provider
184 Binding(
188 Binding(
185 auto_suggest.accept_in_vi_insert_mode,
189 auto_suggest.accept_in_vi_insert_mode,
186 ["end"],
190 ["end"],
187 "default_buffer_focused & (ebivim | ~vi_insert_mode)",
191 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
188 ),
192 ),
189 Binding(
193 Binding(
190 auto_suggest.accept_in_vi_insert_mode,
194 auto_suggest.accept_in_vi_insert_mode,
191 ["c-e"],
195 ["c-e"],
192 "vi_insert_mode & default_buffer_focused & ebivim",
196 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
197 ),
198 Binding(
199 auto_suggest.accept,
200 ["c-f"],
201 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
202 ),
203 Binding(
204 auto_suggest.accept,
205 ["right"],
206 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
193 ),
207 ),
194 Binding(auto_suggest.accept, ["c-f"], "vi_insert_mode & default_buffer_focused"),
195 Binding(
208 Binding(
196 auto_suggest.accept_word,
209 auto_suggest.accept_word,
197 ["escape", "f"],
210 ["escape", "f"],
198 "vi_insert_mode & default_buffer_focused & ebivim",
211 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
199 ),
212 ),
200 Binding(
213 Binding(
201 auto_suggest.accept_token,
214 auto_suggest.accept_token,
202 ["c-right"],
215 ["c-right"],
203 "has_suggestion & default_buffer_focused",
216 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
204 ),
217 ),
205 Binding(
218 Binding(
206 auto_suggest.discard,
219 auto_suggest.discard,
207 ["escape"],
220 ["escape"],
221 # note this one is using `emacs_insert_mode`, not `emacs_like_insert_mode`
222 # as in `vi_insert_mode` we do not want `escape` to be shadowed (ever).
208 "has_suggestion & default_buffer_focused & emacs_insert_mode",
223 "has_suggestion & default_buffer_focused & emacs_insert_mode",
209 ),
224 ),
210 Binding(
225 Binding(
211 auto_suggest.swap_autosuggestion_up,
226 auto_suggest.swap_autosuggestion_up,
212 ["up"],
227 ["up"],
213 "navigable_suggestions"
228 "navigable_suggestions"
214 " & ~has_line_above"
229 " & ~has_line_above"
215 " & has_suggestion"
230 " & has_suggestion"
216 " & default_buffer_focused",
231 " & default_buffer_focused",
217 ),
232 ),
218 Binding(
233 Binding(
219 auto_suggest.swap_autosuggestion_down,
234 auto_suggest.swap_autosuggestion_down,
220 ["down"],
235 ["down"],
221 "navigable_suggestions"
236 "navigable_suggestions"
222 " & ~has_line_below"
237 " & ~has_line_below"
223 " & has_suggestion"
238 " & has_suggestion"
224 " & default_buffer_focused",
239 " & default_buffer_focused",
225 ),
240 ),
226 Binding(
241 Binding(
227 auto_suggest.up_and_update_hint,
242 auto_suggest.up_and_update_hint,
228 ["up"],
243 ["up"],
229 "has_line_above & navigable_suggestions & default_buffer_focused",
244 "has_line_above & navigable_suggestions & default_buffer_focused",
230 ),
245 ),
231 Binding(
246 Binding(
232 auto_suggest.down_and_update_hint,
247 auto_suggest.down_and_update_hint,
233 ["down"],
248 ["down"],
234 "has_line_below & navigable_suggestions & default_buffer_focused",
249 "has_line_below & navigable_suggestions & default_buffer_focused",
235 ),
250 ),
236 Binding(
251 Binding(
237 auto_suggest.accept_character,
252 auto_suggest.accept_character,
238 ["escape", "right"],
253 ["escape", "right"],
239 "has_suggestion & default_buffer_focused",
254 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
240 ),
255 ),
241 Binding(
256 Binding(
242 auto_suggest.accept_and_move_cursor_left,
257 auto_suggest.accept_and_move_cursor_left,
243 ["c-left"],
258 ["c-left"],
244 "has_suggestion & default_buffer_focused",
259 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
245 ),
260 ),
246 Binding(
261 Binding(
247 auto_suggest.accept_and_keep_cursor,
262 auto_suggest.accept_and_keep_cursor,
248 ["c-down"],
263 ["c-down"],
249 "has_suggestion & default_buffer_focused",
264 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
250 ),
265 ),
251 Binding(
266 Binding(
252 auto_suggest.backspace_and_resume_hint,
267 auto_suggest.backspace_and_resume_hint,
253 ["backspace"],
268 ["backspace"],
254 "has_suggestion & default_buffer_focused",
269 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
255 ),
270 ),
256 ]
271 ]
257
272
258
273
259 SIMPLE_CONTROL_BINDINGS = [
274 SIMPLE_CONTROL_BINDINGS = [
260 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
275 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
261 for key, cmd in {
276 for key, cmd in {
262 "c-a": nc.beginning_of_line,
277 "c-a": nc.beginning_of_line,
263 "c-b": nc.backward_char,
278 "c-b": nc.backward_char,
264 "c-k": nc.kill_line,
279 "c-k": nc.kill_line,
265 "c-w": nc.backward_kill_word,
280 "c-w": nc.backward_kill_word,
266 "c-y": nc.yank,
281 "c-y": nc.yank,
267 "c-_": nc.undo,
282 "c-_": nc.undo,
268 }.items()
283 }.items()
269 ]
284 ]
270
285
271
286
272 ALT_AND_COMOBO_CONTROL_BINDINGS = [
287 ALT_AND_COMOBO_CONTROL_BINDINGS = [
273 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
288 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
274 for keys, cmd in {
289 for keys, cmd in {
275 # Control Combos
290 # Control Combos
276 ("c-x", "c-e"): nc.edit_and_execute,
291 ("c-x", "c-e"): nc.edit_and_execute,
277 ("c-x", "e"): nc.edit_and_execute,
292 ("c-x", "e"): nc.edit_and_execute,
278 # Alt
293 # Alt
279 ("escape", "b"): nc.backward_word,
294 ("escape", "b"): nc.backward_word,
280 ("escape", "c"): nc.capitalize_word,
295 ("escape", "c"): nc.capitalize_word,
281 ("escape", "d"): nc.kill_word,
296 ("escape", "d"): nc.kill_word,
282 ("escape", "h"): nc.backward_kill_word,
297 ("escape", "h"): nc.backward_kill_word,
283 ("escape", "l"): nc.downcase_word,
298 ("escape", "l"): nc.downcase_word,
284 ("escape", "u"): nc.uppercase_word,
299 ("escape", "u"): nc.uppercase_word,
285 ("escape", "y"): nc.yank_pop,
300 ("escape", "y"): nc.yank_pop,
286 ("escape", "."): nc.yank_last_arg,
301 ("escape", "."): nc.yank_last_arg,
287 }.items()
302 }.items()
288 ]
303 ]
289
304
290
305
291 def add_binding(bindings: KeyBindings, binding: Binding):
306 def add_binding(bindings: KeyBindings, binding: Binding):
292 bindings.add(
307 bindings.add(
293 *binding.keys,
308 *binding.keys,
294 **({"filter": binding.filter} if binding.filter is not None else {}),
309 **({"filter": binding.filter} if binding.filter is not None else {}),
295 )(binding.command)
310 )(binding.command)
296
311
297
312
298 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
313 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
299 """Set up the prompt_toolkit keyboard shortcuts for IPython.
314 """Set up the prompt_toolkit keyboard shortcuts for IPython.
300
315
301 Parameters
316 Parameters
302 ----------
317 ----------
303 shell: InteractiveShell
318 shell: InteractiveShell
304 The current IPython shell Instance
319 The current IPython shell Instance
305 skip: List[Binding]
320 skip: List[Binding]
306 Bindings to skip.
321 Bindings to skip.
307
322
308 Returns
323 Returns
309 -------
324 -------
310 KeyBindings
325 KeyBindings
311 the keybinding instance for prompt toolkit.
326 the keybinding instance for prompt toolkit.
312
327
313 """
328 """
314 kb = KeyBindings()
329 kb = KeyBindings()
315 skip = skip or []
330 skip = skip or []
316 for binding in KEY_BINDINGS:
331 for binding in KEY_BINDINGS:
317 skip_this_one = False
332 skip_this_one = False
318 for to_skip in skip:
333 for to_skip in skip:
319 if (
334 if (
320 to_skip.command == binding.command
335 to_skip.command == binding.command
321 and to_skip.filter == binding.filter
336 and to_skip.filter == binding.filter
322 and to_skip.keys == binding.keys
337 and to_skip.keys == binding.keys
323 ):
338 ):
324 skip_this_one = True
339 skip_this_one = True
325 break
340 break
326 if skip_this_one:
341 if skip_this_one:
327 continue
342 continue
328 add_binding(kb, binding)
343 add_binding(kb, binding)
329
344
330 def get_input_mode(self):
345 def get_input_mode(self):
331 app = get_app()
346 app = get_app()
332 app.ttimeoutlen = shell.ttimeoutlen
347 app.ttimeoutlen = shell.ttimeoutlen
333 app.timeoutlen = shell.timeoutlen
348 app.timeoutlen = shell.timeoutlen
334
349
335 return self._input_mode
350 return self._input_mode
336
351
337 def set_input_mode(self, mode):
352 def set_input_mode(self, mode):
338 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
353 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
339 cursor = "\x1b[{} q".format(shape)
354 cursor = "\x1b[{} q".format(shape)
340
355
341 sys.stdout.write(cursor)
356 sys.stdout.write(cursor)
342 sys.stdout.flush()
357 sys.stdout.flush()
343
358
344 self._input_mode = mode
359 self._input_mode = mode
345
360
346 if shell.editing_mode == "vi" and shell.modal_cursor:
361 if shell.editing_mode == "vi" and shell.modal_cursor:
347 ViState._input_mode = InputMode.INSERT # type: ignore
362 ViState._input_mode = InputMode.INSERT # type: ignore
348 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
363 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
349
364
350 return kb
365 return kb
351
366
352
367
353 def reformat_and_execute(event):
368 def reformat_and_execute(event):
354 """Reformat code and execute it"""
369 """Reformat code and execute it"""
355 shell = get_ipython()
370 shell = get_ipython()
356 reformat_text_before_cursor(
371 reformat_text_before_cursor(
357 event.current_buffer, event.current_buffer.document, shell
372 event.current_buffer, event.current_buffer.document, shell
358 )
373 )
359 event.current_buffer.validate_and_handle()
374 event.current_buffer.validate_and_handle()
360
375
361
376
362 def reformat_text_before_cursor(buffer, document, shell):
377 def reformat_text_before_cursor(buffer, document, shell):
363 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
378 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
364 try:
379 try:
365 formatted_text = shell.reformat_handler(text)
380 formatted_text = shell.reformat_handler(text)
366 buffer.insert_text(formatted_text)
381 buffer.insert_text(formatted_text)
367 except Exception as e:
382 except Exception as e:
368 buffer.insert_text(text)
383 buffer.insert_text(text)
369
384
370
385
371 def handle_return_or_newline_or_execute(event):
386 def handle_return_or_newline_or_execute(event):
372 shell = get_ipython()
387 shell = get_ipython()
373 if getattr(shell, "handle_return", None):
388 if getattr(shell, "handle_return", None):
374 return shell.handle_return(shell)(event)
389 return shell.handle_return(shell)(event)
375 else:
390 else:
376 return newline_or_execute_outer(shell)(event)
391 return newline_or_execute_outer(shell)(event)
377
392
378
393
379 def newline_or_execute_outer(shell):
394 def newline_or_execute_outer(shell):
380 def newline_or_execute(event):
395 def newline_or_execute(event):
381 """When the user presses return, insert a newline or execute the code."""
396 """When the user presses return, insert a newline or execute the code."""
382 b = event.current_buffer
397 b = event.current_buffer
383 d = b.document
398 d = b.document
384
399
385 if b.complete_state:
400 if b.complete_state:
386 cc = b.complete_state.current_completion
401 cc = b.complete_state.current_completion
387 if cc:
402 if cc:
388 b.apply_completion(cc)
403 b.apply_completion(cc)
389 else:
404 else:
390 b.cancel_completion()
405 b.cancel_completion()
391 return
406 return
392
407
393 # If there's only one line, treat it as if the cursor is at the end.
408 # If there's only one line, treat it as if the cursor is at the end.
394 # See https://github.com/ipython/ipython/issues/10425
409 # See https://github.com/ipython/ipython/issues/10425
395 if d.line_count == 1:
410 if d.line_count == 1:
396 check_text = d.text
411 check_text = d.text
397 else:
412 else:
398 check_text = d.text[: d.cursor_position]
413 check_text = d.text[: d.cursor_position]
399 status, indent = shell.check_complete(check_text)
414 status, indent = shell.check_complete(check_text)
400
415
401 # if all we have after the cursor is whitespace: reformat current text
416 # if all we have after the cursor is whitespace: reformat current text
402 # before cursor
417 # before cursor
403 after_cursor = d.text[d.cursor_position :]
418 after_cursor = d.text[d.cursor_position :]
404 reformatted = False
419 reformatted = False
405 if not after_cursor.strip():
420 if not after_cursor.strip():
406 reformat_text_before_cursor(b, d, shell)
421 reformat_text_before_cursor(b, d, shell)
407 reformatted = True
422 reformatted = True
408 if not (
423 if not (
409 d.on_last_line
424 d.on_last_line
410 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
425 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
411 ):
426 ):
412 if shell.autoindent:
427 if shell.autoindent:
413 b.insert_text("\n" + indent)
428 b.insert_text("\n" + indent)
414 else:
429 else:
415 b.insert_text("\n")
430 b.insert_text("\n")
416 return
431 return
417
432
418 if (status != "incomplete") and b.accept_handler:
433 if (status != "incomplete") and b.accept_handler:
419 if not reformatted:
434 if not reformatted:
420 reformat_text_before_cursor(b, d, shell)
435 reformat_text_before_cursor(b, d, shell)
421 b.validate_and_handle()
436 b.validate_and_handle()
422 else:
437 else:
423 if shell.autoindent:
438 if shell.autoindent:
424 b.insert_text("\n" + indent)
439 b.insert_text("\n" + indent)
425 else:
440 else:
426 b.insert_text("\n")
441 b.insert_text("\n")
427
442
428 return newline_or_execute
443 return newline_or_execute
429
444
430
445
431 def previous_history_or_previous_completion(event):
446 def previous_history_or_previous_completion(event):
432 """
447 """
433 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
448 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
434
449
435 If completer is open this still select previous completion.
450 If completer is open this still select previous completion.
436 """
451 """
437 event.current_buffer.auto_up()
452 event.current_buffer.auto_up()
438
453
439
454
440 def next_history_or_next_completion(event):
455 def next_history_or_next_completion(event):
441 """
456 """
442 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
457 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
443
458
444 If completer is open this still select next completion.
459 If completer is open this still select next completion.
445 """
460 """
446 event.current_buffer.auto_down()
461 event.current_buffer.auto_down()
447
462
448
463
449 def dismiss_completion(event):
464 def dismiss_completion(event):
450 """Dismiss completion"""
465 """Dismiss completion"""
451 b = event.current_buffer
466 b = event.current_buffer
452 if b.complete_state:
467 if b.complete_state:
453 b.cancel_completion()
468 b.cancel_completion()
454
469
455
470
456 def reset_buffer(event):
471 def reset_buffer(event):
457 """Reset buffer"""
472 """Reset buffer"""
458 b = event.current_buffer
473 b = event.current_buffer
459 if b.complete_state:
474 if b.complete_state:
460 b.cancel_completion()
475 b.cancel_completion()
461 else:
476 else:
462 b.reset()
477 b.reset()
463
478
464
479
465 def reset_search_buffer(event):
480 def reset_search_buffer(event):
466 """Reset search buffer"""
481 """Reset search buffer"""
467 if event.current_buffer.document.text:
482 if event.current_buffer.document.text:
468 event.current_buffer.reset()
483 event.current_buffer.reset()
469 else:
484 else:
470 event.app.layout.focus(DEFAULT_BUFFER)
485 event.app.layout.focus(DEFAULT_BUFFER)
471
486
472
487
473 def suspend_to_bg(event):
488 def suspend_to_bg(event):
474 """Suspend to background"""
489 """Suspend to background"""
475 event.app.suspend_to_background()
490 event.app.suspend_to_background()
476
491
477
492
478 def quit(event):
493 def quit(event):
479 """
494 """
480 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
495 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
481
496
482 On platforms that support SIGQUIT, send SIGQUIT to the current process.
497 On platforms that support SIGQUIT, send SIGQUIT to the current process.
483 On other platforms, just exit the process with a message.
498 On other platforms, just exit the process with a message.
484 """
499 """
485 sigquit = getattr(signal, "SIGQUIT", None)
500 sigquit = getattr(signal, "SIGQUIT", None)
486 if sigquit is not None:
501 if sigquit is not None:
487 os.kill(0, signal.SIGQUIT)
502 os.kill(0, signal.SIGQUIT)
488 else:
503 else:
489 sys.exit("Quit")
504 sys.exit("Quit")
490
505
491
506
492 def indent_buffer(event):
507 def indent_buffer(event):
493 """Indent buffer"""
508 """Indent buffer"""
494 event.current_buffer.insert_text(" " * 4)
509 event.current_buffer.insert_text(" " * 4)
495
510
496
511
497 def newline_autoindent(event):
512 def newline_autoindent(event):
498 """Insert a newline after the cursor indented appropriately.
513 """Insert a newline after the cursor indented appropriately.
499
514
500 Fancier version of former ``newline_with_copy_margin`` which should
515 Fancier version of former ``newline_with_copy_margin`` which should
501 compute the correct indentation of the inserted line. That is to say, indent
516 compute the correct indentation of the inserted line. That is to say, indent
502 by 4 extra space after a function definition, class definition, context
517 by 4 extra space after a function definition, class definition, context
503 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
518 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
504 """
519 """
505 shell = get_ipython()
520 shell = get_ipython()
506 inputsplitter = shell.input_transformer_manager
521 inputsplitter = shell.input_transformer_manager
507 b = event.current_buffer
522 b = event.current_buffer
508 d = b.document
523 d = b.document
509
524
510 if b.complete_state:
525 if b.complete_state:
511 b.cancel_completion()
526 b.cancel_completion()
512 text = d.text[: d.cursor_position] + "\n"
527 text = d.text[: d.cursor_position] + "\n"
513 _, indent = inputsplitter.check_complete(text)
528 _, indent = inputsplitter.check_complete(text)
514 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
529 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
515
530
516
531
517 def open_input_in_editor(event):
532 def open_input_in_editor(event):
518 """Open code from input in external editor"""
533 """Open code from input in external editor"""
519 event.app.current_buffer.open_in_editor()
534 event.app.current_buffer.open_in_editor()
520
535
521
536
522 if sys.platform == "win32":
537 if sys.platform == "win32":
523 from IPython.core.error import TryNext
538 from IPython.core.error import TryNext
524 from IPython.lib.clipboard import (
539 from IPython.lib.clipboard import (
525 ClipboardEmpty,
540 ClipboardEmpty,
526 tkinter_clipboard_get,
541 tkinter_clipboard_get,
527 win32_clipboard_get,
542 win32_clipboard_get,
528 )
543 )
529
544
530 @undoc
545 @undoc
531 def win_paste(event):
546 def win_paste(event):
532 try:
547 try:
533 text = win32_clipboard_get()
548 text = win32_clipboard_get()
534 except TryNext:
549 except TryNext:
535 try:
550 try:
536 text = tkinter_clipboard_get()
551 text = tkinter_clipboard_get()
537 except (TryNext, ClipboardEmpty):
552 except (TryNext, ClipboardEmpty):
538 return
553 return
539 except ClipboardEmpty:
554 except ClipboardEmpty:
540 return
555 return
541 event.current_buffer.insert_text(text.replace("\t", " " * 4))
556 event.current_buffer.insert_text(text.replace("\t", " " * 4))
542
557
543 else:
558 else:
544
559
545 @undoc
560 @undoc
546 def win_paste(event):
561 def win_paste(event):
547 """Stub used on other platforms"""
562 """Stub used on other platforms"""
548 pass
563 pass
549
564
550
565
551 KEY_BINDINGS = [
566 KEY_BINDINGS = [
552 Binding(
567 Binding(
553 handle_return_or_newline_or_execute,
568 handle_return_or_newline_or_execute,
554 ["enter"],
569 ["enter"],
555 "default_buffer_focused & ~has_selection & insert_mode",
570 "default_buffer_focused & ~has_selection & insert_mode",
556 ),
571 ),
557 Binding(
572 Binding(
558 reformat_and_execute,
573 reformat_and_execute,
559 ["escape", "enter"],
574 ["escape", "enter"],
560 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
575 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
561 ),
576 ),
562 Binding(quit, ["c-\\"]),
577 Binding(quit, ["c-\\"]),
563 Binding(
578 Binding(
564 previous_history_or_previous_completion,
579 previous_history_or_previous_completion,
565 ["c-p"],
580 ["c-p"],
566 "vi_insert_mode & default_buffer_focused",
581 "vi_insert_mode & default_buffer_focused",
567 ),
582 ),
568 Binding(
583 Binding(
569 next_history_or_next_completion,
584 next_history_or_next_completion,
570 ["c-n"],
585 ["c-n"],
571 "vi_insert_mode & default_buffer_focused",
586 "vi_insert_mode & default_buffer_focused",
572 ),
587 ),
573 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
588 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
574 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
589 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
575 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
590 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
576 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
591 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
577 Binding(
592 Binding(
578 indent_buffer,
593 indent_buffer,
579 ["tab"], # Ctrl+I == Tab
594 ["tab"], # Ctrl+I == Tab
580 "default_buffer_focused"
595 "default_buffer_focused"
581 " & ~has_selection"
596 " & ~has_selection"
582 " & insert_mode"
597 " & insert_mode"
583 " & cursor_in_leading_ws",
598 " & cursor_in_leading_ws",
584 ),
599 ),
585 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
600 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
586 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
601 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
587 *AUTO_MATCH_BINDINGS,
602 *AUTO_MATCH_BINDINGS,
588 *AUTO_SUGGEST_BINDINGS,
603 *AUTO_SUGGEST_BINDINGS,
589 Binding(
604 Binding(
590 display_completions_like_readline,
605 display_completions_like_readline,
591 ["c-i"],
606 ["c-i"],
592 "readline_like_completions"
607 "readline_like_completions"
593 " & default_buffer_focused"
608 " & default_buffer_focused"
594 " & ~has_selection"
609 " & ~has_selection"
595 " & insert_mode"
610 " & insert_mode"
596 " & ~cursor_in_leading_ws",
611 " & ~cursor_in_leading_ws",
597 ),
612 ),
598 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
613 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
599 *SIMPLE_CONTROL_BINDINGS,
614 *SIMPLE_CONTROL_BINDINGS,
600 *ALT_AND_COMOBO_CONTROL_BINDINGS,
615 *ALT_AND_COMOBO_CONTROL_BINDINGS,
601 ]
616 ]
@@ -1,375 +1,383 b''
1 import re
1 import re
2 import tokenize
2 import tokenize
3 from io import StringIO
3 from io import StringIO
4 from typing import Callable, List, Optional, Union, Generator, Tuple
4 from typing import Callable, List, Optional, Union, Generator, Tuple
5
5
6 from prompt_toolkit.buffer import Buffer
6 from prompt_toolkit.buffer import Buffer
7 from prompt_toolkit.key_binding import KeyPressEvent
7 from prompt_toolkit.key_binding import KeyPressEvent
8 from prompt_toolkit.key_binding.bindings import named_commands as nc
8 from prompt_toolkit.key_binding.bindings import named_commands as nc
9 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
9 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
10 from prompt_toolkit.document import Document
10 from prompt_toolkit.document import Document
11 from prompt_toolkit.history import History
11 from prompt_toolkit.history import History
12 from prompt_toolkit.shortcuts import PromptSession
12 from prompt_toolkit.shortcuts import PromptSession
13 from prompt_toolkit.layout.processors import (
13 from prompt_toolkit.layout.processors import (
14 Processor,
14 Processor,
15 Transformation,
15 Transformation,
16 TransformationInput,
16 TransformationInput,
17 )
17 )
18
18
19 from IPython.core.getipython import get_ipython
19 from IPython.core.getipython import get_ipython
20 from IPython.utils.tokenutil import generate_tokens
20 from IPython.utils.tokenutil import generate_tokens
21
21
22
22
23 def _get_query(document: Document):
23 def _get_query(document: Document):
24 return document.lines[document.cursor_position_row]
24 return document.lines[document.cursor_position_row]
25
25
26
26
27 class AppendAutoSuggestionInAnyLine(Processor):
27 class AppendAutoSuggestionInAnyLine(Processor):
28 """
28 """
29 Append the auto suggestion to lines other than the last (appending to the
29 Append the auto suggestion to lines other than the last (appending to the
30 last line is natively supported by the prompt toolkit).
30 last line is natively supported by the prompt toolkit).
31 """
31 """
32
32
33 def __init__(self, style: str = "class:auto-suggestion") -> None:
33 def __init__(self, style: str = "class:auto-suggestion") -> None:
34 self.style = style
34 self.style = style
35
35
36 def apply_transformation(self, ti: TransformationInput) -> Transformation:
36 def apply_transformation(self, ti: TransformationInput) -> Transformation:
37 is_last_line = ti.lineno == ti.document.line_count - 1
37 is_last_line = ti.lineno == ti.document.line_count - 1
38 is_active_line = ti.lineno == ti.document.cursor_position_row
38 is_active_line = ti.lineno == ti.document.cursor_position_row
39
39
40 if not is_last_line and is_active_line:
40 if not is_last_line and is_active_line:
41 buffer = ti.buffer_control.buffer
41 buffer = ti.buffer_control.buffer
42
42
43 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
43 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
44 suggestion = buffer.suggestion.text
44 suggestion = buffer.suggestion.text
45 else:
45 else:
46 suggestion = ""
46 suggestion = ""
47
47
48 return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
48 return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
49 else:
49 else:
50 return Transformation(fragments=ti.fragments)
50 return Transformation(fragments=ti.fragments)
51
51
52
52
53 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
53 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
54 """
54 """
55 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
55 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
56 suggestion from history. To do so it remembers the current position, but it
56 suggestion from history. To do so it remembers the current position, but it
57 state need to carefully be cleared on the right events.
57 state need to carefully be cleared on the right events.
58 """
58 """
59
59
60 def __init__(
60 def __init__(
61 self,
61 self,
62 ):
62 ):
63 self.skip_lines = 0
63 self.skip_lines = 0
64 self._connected_apps = []
64 self._connected_apps = []
65
65
66 def reset_history_position(self, _: Buffer):
66 def reset_history_position(self, _: Buffer):
67 self.skip_lines = 0
67 self.skip_lines = 0
68
68
69 def disconnect(self):
69 def disconnect(self):
70 for pt_app in self._connected_apps:
70 for pt_app in self._connected_apps:
71 text_insert_event = pt_app.default_buffer.on_text_insert
71 text_insert_event = pt_app.default_buffer.on_text_insert
72 text_insert_event.remove_handler(self.reset_history_position)
72 text_insert_event.remove_handler(self.reset_history_position)
73
73
74 def connect(self, pt_app: PromptSession):
74 def connect(self, pt_app: PromptSession):
75 self._connected_apps.append(pt_app)
75 self._connected_apps.append(pt_app)
76 # note: `on_text_changed` could be used for a bit different behaviour
76 # note: `on_text_changed` could be used for a bit different behaviour
77 # on character deletion (i.e. reseting history position on backspace)
77 # on character deletion (i.e. reseting history position on backspace)
78 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
78 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
79 pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
79 pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
80
80
81 def get_suggestion(
81 def get_suggestion(
82 self, buffer: Buffer, document: Document
82 self, buffer: Buffer, document: Document
83 ) -> Optional[Suggestion]:
83 ) -> Optional[Suggestion]:
84 text = _get_query(document)
84 text = _get_query(document)
85
85
86 if text.strip():
86 if text.strip():
87 for suggestion, _ in self._find_next_match(
87 for suggestion, _ in self._find_next_match(
88 text, self.skip_lines, buffer.history
88 text, self.skip_lines, buffer.history
89 ):
89 ):
90 return Suggestion(suggestion)
90 return Suggestion(suggestion)
91
91
92 return None
92 return None
93
93
94 def _dismiss(self, buffer, *args, **kwargs):
94 def _dismiss(self, buffer, *args, **kwargs):
95 buffer.suggestion = None
95 buffer.suggestion = None
96
96
97 def _find_match(
97 def _find_match(
98 self, text: str, skip_lines: float, history: History, previous: bool
98 self, text: str, skip_lines: float, history: History, previous: bool
99 ) -> Generator[Tuple[str, float], None, None]:
99 ) -> Generator[Tuple[str, float], None, None]:
100 """
100 """
101 text : str
101 text : str
102 Text content to find a match for, the user cursor is most of the
102 Text content to find a match for, the user cursor is most of the
103 time at the end of this text.
103 time at the end of this text.
104 skip_lines : float
104 skip_lines : float
105 number of items to skip in the search, this is used to indicate how
105 number of items to skip in the search, this is used to indicate how
106 far in the list the user has navigated by pressing up or down.
106 far in the list the user has navigated by pressing up or down.
107 The float type is used as the base value is +inf
107 The float type is used as the base value is +inf
108 history : History
108 history : History
109 prompt_toolkit History instance to fetch previous entries from.
109 prompt_toolkit History instance to fetch previous entries from.
110 previous : bool
110 previous : bool
111 Direction of the search, whether we are looking previous match
111 Direction of the search, whether we are looking previous match
112 (True), or next match (False).
112 (True), or next match (False).
113
113
114 Yields
114 Yields
115 ------
115 ------
116 Tuple with:
116 Tuple with:
117 str:
117 str:
118 current suggestion.
118 current suggestion.
119 float:
119 float:
120 will actually yield only ints, which is passed back via skip_lines,
120 will actually yield only ints, which is passed back via skip_lines,
121 which may be a +inf (float)
121 which may be a +inf (float)
122
122
123
123
124 """
124 """
125 line_number = -1
125 line_number = -1
126 for string in reversed(list(history.get_strings())):
126 for string in reversed(list(history.get_strings())):
127 for line in reversed(string.splitlines()):
127 for line in reversed(string.splitlines()):
128 line_number += 1
128 line_number += 1
129 if not previous and line_number < skip_lines:
129 if not previous and line_number < skip_lines:
130 continue
130 continue
131 # do not return empty suggestions as these
131 # do not return empty suggestions as these
132 # close the auto-suggestion overlay (and are useless)
132 # close the auto-suggestion overlay (and are useless)
133 if line.startswith(text) and len(line) > len(text):
133 if line.startswith(text) and len(line) > len(text):
134 yield line[len(text) :], line_number
134 yield line[len(text) :], line_number
135 if previous and line_number >= skip_lines:
135 if previous and line_number >= skip_lines:
136 return
136 return
137
137
138 def _find_next_match(
138 def _find_next_match(
139 self, text: str, skip_lines: float, history: History
139 self, text: str, skip_lines: float, history: History
140 ) -> Generator[Tuple[str, float], None, None]:
140 ) -> Generator[Tuple[str, float], None, None]:
141 return self._find_match(text, skip_lines, history, previous=False)
141 return self._find_match(text, skip_lines, history, previous=False)
142
142
143 def _find_previous_match(self, text: str, skip_lines: float, history: History):
143 def _find_previous_match(self, text: str, skip_lines: float, history: History):
144 return reversed(
144 return reversed(
145 list(self._find_match(text, skip_lines, history, previous=True))
145 list(self._find_match(text, skip_lines, history, previous=True))
146 )
146 )
147
147
148 def up(self, query: str, other_than: str, history: History) -> None:
148 def up(self, query: str, other_than: str, history: History) -> None:
149 for suggestion, line_number in self._find_next_match(
149 for suggestion, line_number in self._find_next_match(
150 query, self.skip_lines, history
150 query, self.skip_lines, history
151 ):
151 ):
152 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
152 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
153 # we want to switch from 'very.b' to 'very.a' because a) if the
153 # we want to switch from 'very.b' to 'very.a' because a) if the
154 # suggestion equals current text, prompt-toolkit aborts suggesting
154 # suggestion equals current text, prompt-toolkit aborts suggesting
155 # b) user likely would not be interested in 'very' anyways (they
155 # b) user likely would not be interested in 'very' anyways (they
156 # already typed it).
156 # already typed it).
157 if query + suggestion != other_than:
157 if query + suggestion != other_than:
158 self.skip_lines = line_number
158 self.skip_lines = line_number
159 break
159 break
160 else:
160 else:
161 # no matches found, cycle back to beginning
161 # no matches found, cycle back to beginning
162 self.skip_lines = 0
162 self.skip_lines = 0
163
163
164 def down(self, query: str, other_than: str, history: History) -> None:
164 def down(self, query: str, other_than: str, history: History) -> None:
165 for suggestion, line_number in self._find_previous_match(
165 for suggestion, line_number in self._find_previous_match(
166 query, self.skip_lines, history
166 query, self.skip_lines, history
167 ):
167 ):
168 if query + suggestion != other_than:
168 if query + suggestion != other_than:
169 self.skip_lines = line_number
169 self.skip_lines = line_number
170 break
170 break
171 else:
171 else:
172 # no matches found, cycle to end
172 # no matches found, cycle to end
173 for suggestion, line_number in self._find_previous_match(
173 for suggestion, line_number in self._find_previous_match(
174 query, float("Inf"), history
174 query, float("Inf"), history
175 ):
175 ):
176 if query + suggestion != other_than:
176 if query + suggestion != other_than:
177 self.skip_lines = line_number
177 self.skip_lines = line_number
178 break
178 break
179
179
180
180
181 # Needed for to accept autosuggestions in vi insert mode
181 def accept_or_jump_to_end(event: KeyPressEvent):
182 def accept_in_vi_insert_mode(event: KeyPressEvent):
182 """Apply autosuggestion or jump to end of line."""
183 """Apply autosuggestion if at end of line."""
184 buffer = event.current_buffer
183 buffer = event.current_buffer
185 d = buffer.document
184 d = buffer.document
186 after_cursor = d.text[d.cursor_position :]
185 after_cursor = d.text[d.cursor_position :]
187 lines = after_cursor.split("\n")
186 lines = after_cursor.split("\n")
188 end_of_current_line = lines[0].strip()
187 end_of_current_line = lines[0].strip()
189 suggestion = buffer.suggestion
188 suggestion = buffer.suggestion
190 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
189 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
191 buffer.insert_text(suggestion.text)
190 buffer.insert_text(suggestion.text)
192 else:
191 else:
193 nc.end_of_line(event)
192 nc.end_of_line(event)
194
193
195
194
195 def accept_in_vi_insert_mode(event: KeyPressEvent):
196 """Accept autosuggestion or jump to end of line.
197
198 .. deprecated:: 8.12
199 Use `accept_or_jump_to_end` instead.
200 """
201 return accept_or_jump_to_end(event)
202
203
196 def accept(event: KeyPressEvent):
204 def accept(event: KeyPressEvent):
197 """Accept autosuggestion"""
205 """Accept autosuggestion"""
198 buffer = event.current_buffer
206 buffer = event.current_buffer
199 suggestion = buffer.suggestion
207 suggestion = buffer.suggestion
200 if suggestion:
208 if suggestion:
201 buffer.insert_text(suggestion.text)
209 buffer.insert_text(suggestion.text)
202 else:
210 else:
203 nc.forward_char(event)
211 nc.forward_char(event)
204
212
205
213
206 def discard(event: KeyPressEvent):
214 def discard(event: KeyPressEvent):
207 """Discard autosuggestion"""
215 """Discard autosuggestion"""
208 buffer = event.current_buffer
216 buffer = event.current_buffer
209 buffer.suggestion = None
217 buffer.suggestion = None
210
218
211
219
212 def accept_word(event: KeyPressEvent):
220 def accept_word(event: KeyPressEvent):
213 """Fill partial autosuggestion by word"""
221 """Fill partial autosuggestion by word"""
214 buffer = event.current_buffer
222 buffer = event.current_buffer
215 suggestion = buffer.suggestion
223 suggestion = buffer.suggestion
216 if suggestion:
224 if suggestion:
217 t = re.split(r"(\S+\s+)", suggestion.text)
225 t = re.split(r"(\S+\s+)", suggestion.text)
218 buffer.insert_text(next((x for x in t if x), ""))
226 buffer.insert_text(next((x for x in t if x), ""))
219 else:
227 else:
220 nc.forward_word(event)
228 nc.forward_word(event)
221
229
222
230
223 def accept_character(event: KeyPressEvent):
231 def accept_character(event: KeyPressEvent):
224 """Fill partial autosuggestion by character"""
232 """Fill partial autosuggestion by character"""
225 b = event.current_buffer
233 b = event.current_buffer
226 suggestion = b.suggestion
234 suggestion = b.suggestion
227 if suggestion and suggestion.text:
235 if suggestion and suggestion.text:
228 b.insert_text(suggestion.text[0])
236 b.insert_text(suggestion.text[0])
229
237
230
238
231 def accept_and_keep_cursor(event: KeyPressEvent):
239 def accept_and_keep_cursor(event: KeyPressEvent):
232 """Accept autosuggestion and keep cursor in place"""
240 """Accept autosuggestion and keep cursor in place"""
233 buffer = event.current_buffer
241 buffer = event.current_buffer
234 old_position = buffer.cursor_position
242 old_position = buffer.cursor_position
235 suggestion = buffer.suggestion
243 suggestion = buffer.suggestion
236 if suggestion:
244 if suggestion:
237 buffer.insert_text(suggestion.text)
245 buffer.insert_text(suggestion.text)
238 buffer.cursor_position = old_position
246 buffer.cursor_position = old_position
239
247
240
248
241 def accept_and_move_cursor_left(event: KeyPressEvent):
249 def accept_and_move_cursor_left(event: KeyPressEvent):
242 """Accept autosuggestion and move cursor left in place"""
250 """Accept autosuggestion and move cursor left in place"""
243 accept_and_keep_cursor(event)
251 accept_and_keep_cursor(event)
244 nc.backward_char(event)
252 nc.backward_char(event)
245
253
246
254
247 def _update_hint(buffer: Buffer):
255 def _update_hint(buffer: Buffer):
248 if buffer.auto_suggest:
256 if buffer.auto_suggest:
249 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
257 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
250 buffer.suggestion = suggestion
258 buffer.suggestion = suggestion
251
259
252
260
253 def backspace_and_resume_hint(event: KeyPressEvent):
261 def backspace_and_resume_hint(event: KeyPressEvent):
254 """Resume autosuggestions after deleting last character"""
262 """Resume autosuggestions after deleting last character"""
255 current_buffer = event.current_buffer
263 current_buffer = event.current_buffer
256
264
257 def resume_hinting(buffer: Buffer):
265 def resume_hinting(buffer: Buffer):
258 _update_hint(buffer)
266 _update_hint(buffer)
259 current_buffer.on_text_changed.remove_handler(resume_hinting)
267 current_buffer.on_text_changed.remove_handler(resume_hinting)
260
268
261 current_buffer.on_text_changed.add_handler(resume_hinting)
269 current_buffer.on_text_changed.add_handler(resume_hinting)
262 nc.backward_delete_char(event)
270 nc.backward_delete_char(event)
263
271
264
272
265 def up_and_update_hint(event: KeyPressEvent):
273 def up_and_update_hint(event: KeyPressEvent):
266 """Go up and update hint"""
274 """Go up and update hint"""
267 current_buffer = event.current_buffer
275 current_buffer = event.current_buffer
268
276
269 current_buffer.auto_up(count=event.arg)
277 current_buffer.auto_up(count=event.arg)
270 _update_hint(current_buffer)
278 _update_hint(current_buffer)
271
279
272
280
273 def down_and_update_hint(event: KeyPressEvent):
281 def down_and_update_hint(event: KeyPressEvent):
274 """Go down and update hint"""
282 """Go down and update hint"""
275 current_buffer = event.current_buffer
283 current_buffer = event.current_buffer
276
284
277 current_buffer.auto_down(count=event.arg)
285 current_buffer.auto_down(count=event.arg)
278 _update_hint(current_buffer)
286 _update_hint(current_buffer)
279
287
280
288
281 def accept_token(event: KeyPressEvent):
289 def accept_token(event: KeyPressEvent):
282 """Fill partial autosuggestion by token"""
290 """Fill partial autosuggestion by token"""
283 b = event.current_buffer
291 b = event.current_buffer
284 suggestion = b.suggestion
292 suggestion = b.suggestion
285
293
286 if suggestion:
294 if suggestion:
287 prefix = _get_query(b.document)
295 prefix = _get_query(b.document)
288 text = prefix + suggestion.text
296 text = prefix + suggestion.text
289
297
290 tokens: List[Optional[str]] = [None, None, None]
298 tokens: List[Optional[str]] = [None, None, None]
291 substrings = [""]
299 substrings = [""]
292 i = 0
300 i = 0
293
301
294 for token in generate_tokens(StringIO(text).readline):
302 for token in generate_tokens(StringIO(text).readline):
295 if token.type == tokenize.NEWLINE:
303 if token.type == tokenize.NEWLINE:
296 index = len(text)
304 index = len(text)
297 else:
305 else:
298 index = text.index(token[1], len(substrings[-1]))
306 index = text.index(token[1], len(substrings[-1]))
299 substrings.append(text[:index])
307 substrings.append(text[:index])
300 tokenized_so_far = substrings[-1]
308 tokenized_so_far = substrings[-1]
301 if tokenized_so_far.startswith(prefix):
309 if tokenized_so_far.startswith(prefix):
302 if i == 0 and len(tokenized_so_far) > len(prefix):
310 if i == 0 and len(tokenized_so_far) > len(prefix):
303 tokens[0] = tokenized_so_far[len(prefix) :]
311 tokens[0] = tokenized_so_far[len(prefix) :]
304 substrings.append(tokenized_so_far)
312 substrings.append(tokenized_so_far)
305 i += 1
313 i += 1
306 tokens[i] = token[1]
314 tokens[i] = token[1]
307 if i == 2:
315 if i == 2:
308 break
316 break
309 i += 1
317 i += 1
310
318
311 if tokens[0]:
319 if tokens[0]:
312 to_insert: str
320 to_insert: str
313 insert_text = substrings[-2]
321 insert_text = substrings[-2]
314 if tokens[1] and len(tokens[1]) == 1:
322 if tokens[1] and len(tokens[1]) == 1:
315 insert_text = substrings[-1]
323 insert_text = substrings[-1]
316 to_insert = insert_text[len(prefix) :]
324 to_insert = insert_text[len(prefix) :]
317 b.insert_text(to_insert)
325 b.insert_text(to_insert)
318 return
326 return
319
327
320 nc.forward_word(event)
328 nc.forward_word(event)
321
329
322
330
323 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
331 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
324
332
325
333
326 def _swap_autosuggestion(
334 def _swap_autosuggestion(
327 buffer: Buffer,
335 buffer: Buffer,
328 provider: NavigableAutoSuggestFromHistory,
336 provider: NavigableAutoSuggestFromHistory,
329 direction_method: Callable,
337 direction_method: Callable,
330 ):
338 ):
331 """
339 """
332 We skip most recent history entry (in either direction) if it equals the
340 We skip most recent history entry (in either direction) if it equals the
333 current autosuggestion because if user cycles when auto-suggestion is shown
341 current autosuggestion because if user cycles when auto-suggestion is shown
334 they most likely want something else than what was suggested (otherwise
342 they most likely want something else than what was suggested (otherwise
335 they would have accepted the suggestion).
343 they would have accepted the suggestion).
336 """
344 """
337 suggestion = buffer.suggestion
345 suggestion = buffer.suggestion
338 if not suggestion:
346 if not suggestion:
339 return
347 return
340
348
341 query = _get_query(buffer.document)
349 query = _get_query(buffer.document)
342 current = query + suggestion.text
350 current = query + suggestion.text
343
351
344 direction_method(query=query, other_than=current, history=buffer.history)
352 direction_method(query=query, other_than=current, history=buffer.history)
345
353
346 new_suggestion = provider.get_suggestion(buffer, buffer.document)
354 new_suggestion = provider.get_suggestion(buffer, buffer.document)
347 buffer.suggestion = new_suggestion
355 buffer.suggestion = new_suggestion
348
356
349
357
350 def swap_autosuggestion_up(event: KeyPressEvent):
358 def swap_autosuggestion_up(event: KeyPressEvent):
351 """Get next autosuggestion from history."""
359 """Get next autosuggestion from history."""
352 shell = get_ipython()
360 shell = get_ipython()
353 provider = shell.auto_suggest
361 provider = shell.auto_suggest
354
362
355 if not isinstance(provider, NavigableAutoSuggestFromHistory):
363 if not isinstance(provider, NavigableAutoSuggestFromHistory):
356 return
364 return
357
365
358 return _swap_autosuggestion(
366 return _swap_autosuggestion(
359 buffer=event.current_buffer, provider=provider, direction_method=provider.up
367 buffer=event.current_buffer, provider=provider, direction_method=provider.up
360 )
368 )
361
369
362
370
363 def swap_autosuggestion_down(event: KeyPressEvent):
371 def swap_autosuggestion_down(event: KeyPressEvent):
364 """Get previous autosuggestion from history."""
372 """Get previous autosuggestion from history."""
365 shell = get_ipython()
373 shell = get_ipython()
366 provider = shell.auto_suggest
374 provider = shell.auto_suggest
367
375
368 if not isinstance(provider, NavigableAutoSuggestFromHistory):
376 if not isinstance(provider, NavigableAutoSuggestFromHistory):
369 return
377 return
370
378
371 return _swap_autosuggestion(
379 return _swap_autosuggestion(
372 buffer=event.current_buffer,
380 buffer=event.current_buffer,
373 provider=provider,
381 provider=provider,
374 direction_method=provider.down,
382 direction_method=provider.down,
375 )
383 )
@@ -1,256 +1,279 b''
1 """
1 """
2 Filters restricting scope of IPython Terminal shortcuts.
2 Filters restricting scope of IPython Terminal shortcuts.
3 """
3 """
4
4
5 # Copyright (c) IPython Development Team.
5 # Copyright (c) IPython Development Team.
6 # Distributed under the terms of the Modified BSD License.
6 # Distributed under the terms of the Modified BSD License.
7
7
8 import ast
8 import ast
9 import re
9 import re
10 import signal
10 import signal
11 import sys
11 import sys
12 from typing import Callable, Dict, Union
12 from typing import Callable, Dict, Union
13
13
14 from prompt_toolkit.application.current import get_app
14 from prompt_toolkit.application.current import get_app
15 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
15 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
16 from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions
16 from prompt_toolkit.filters import Condition, emacs_insert_mode, has_completions
17 from prompt_toolkit.filters import has_focus as has_focus_impl
17 from prompt_toolkit.filters import has_focus as has_focus_impl
18 from prompt_toolkit.filters import (
18 from prompt_toolkit.filters import (
19 Always,
19 Always,
20 has_selection,
20 has_selection,
21 has_suggestion,
21 has_suggestion,
22 vi_insert_mode,
22 vi_insert_mode,
23 vi_mode,
23 vi_mode,
24 )
24 )
25 from prompt_toolkit.layout.layout import FocusableElement
25 from prompt_toolkit.layout.layout import FocusableElement
26
26
27 from IPython.core.getipython import get_ipython
27 from IPython.core.getipython import get_ipython
28 from IPython.core.guarded_eval import _find_dunder, BINARY_OP_DUNDERS, UNARY_OP_DUNDERS
28 from IPython.core.guarded_eval import _find_dunder, BINARY_OP_DUNDERS, UNARY_OP_DUNDERS
29 from IPython.terminal.shortcuts import auto_suggest
29 from IPython.terminal.shortcuts import auto_suggest
30 from IPython.utils.decorators import undoc
30 from IPython.utils.decorators import undoc
31
31
32
32
33 @undoc
33 @undoc
34 @Condition
34 @Condition
35 def cursor_in_leading_ws():
35 def cursor_in_leading_ws():
36 before = get_app().current_buffer.document.current_line_before_cursor
36 before = get_app().current_buffer.document.current_line_before_cursor
37 return (not before) or before.isspace()
37 return (not before) or before.isspace()
38
38
39
39
40 def has_focus(value: FocusableElement):
40 def has_focus(value: FocusableElement):
41 """Wrapper around has_focus adding a nice `__name__` to tester function"""
41 """Wrapper around has_focus adding a nice `__name__` to tester function"""
42 tester = has_focus_impl(value).func
42 tester = has_focus_impl(value).func
43 tester.__name__ = f"is_focused({value})"
43 tester.__name__ = f"is_focused({value})"
44 return Condition(tester)
44 return Condition(tester)
45
45
46
46
47 @undoc
47 @undoc
48 @Condition
48 @Condition
49 def has_line_below() -> bool:
49 def has_line_below() -> bool:
50 document = get_app().current_buffer.document
50 document = get_app().current_buffer.document
51 return document.cursor_position_row < len(document.lines) - 1
51 return document.cursor_position_row < len(document.lines) - 1
52
52
53
53
54 @undoc
54 @undoc
55 @Condition
55 @Condition
56 def has_line_above() -> bool:
56 def has_line_above() -> bool:
57 document = get_app().current_buffer.document
57 document = get_app().current_buffer.document
58 return document.cursor_position_row != 0
58 return document.cursor_position_row != 0
59
59
60
60
61 @Condition
61 @Condition
62 def ebivim():
62 def ebivim():
63 shell = get_ipython()
63 shell = get_ipython()
64 return shell.emacs_bindings_in_vi_insert_mode
64 return shell.emacs_bindings_in_vi_insert_mode
65
65
66
66
67 @Condition
67 @Condition
68 def supports_suspend():
68 def supports_suspend():
69 return hasattr(signal, "SIGTSTP")
69 return hasattr(signal, "SIGTSTP")
70
70
71
71
72 @Condition
72 @Condition
73 def auto_match():
73 def auto_match():
74 shell = get_ipython()
74 shell = get_ipython()
75 return shell.auto_match
75 return shell.auto_match
76
76
77
77
78 def all_quotes_paired(quote, buf):
78 def all_quotes_paired(quote, buf):
79 paired = True
79 paired = True
80 i = 0
80 i = 0
81 while i < len(buf):
81 while i < len(buf):
82 c = buf[i]
82 c = buf[i]
83 if c == quote:
83 if c == quote:
84 paired = not paired
84 paired = not paired
85 elif c == "\\":
85 elif c == "\\":
86 i += 1
86 i += 1
87 i += 1
87 i += 1
88 return paired
88 return paired
89
89
90
90
91 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
91 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
92 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
92 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
93
93
94
94
95 def preceding_text(pattern: Union[str, Callable]):
95 def preceding_text(pattern: Union[str, Callable]):
96 if pattern in _preceding_text_cache:
96 if pattern in _preceding_text_cache:
97 return _preceding_text_cache[pattern]
97 return _preceding_text_cache[pattern]
98
98
99 if callable(pattern):
99 if callable(pattern):
100
100
101 def _preceding_text():
101 def _preceding_text():
102 app = get_app()
102 app = get_app()
103 before_cursor = app.current_buffer.document.current_line_before_cursor
103 before_cursor = app.current_buffer.document.current_line_before_cursor
104 # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
104 # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
105 return bool(pattern(before_cursor)) # type: ignore[operator]
105 return bool(pattern(before_cursor)) # type: ignore[operator]
106
106
107 else:
107 else:
108 m = re.compile(pattern)
108 m = re.compile(pattern)
109
109
110 def _preceding_text():
110 def _preceding_text():
111 app = get_app()
111 app = get_app()
112 before_cursor = app.current_buffer.document.current_line_before_cursor
112 before_cursor = app.current_buffer.document.current_line_before_cursor
113 return bool(m.match(before_cursor))
113 return bool(m.match(before_cursor))
114
114
115 _preceding_text.__name__ = f"preceding_text({pattern!r})"
115 _preceding_text.__name__ = f"preceding_text({pattern!r})"
116
116
117 condition = Condition(_preceding_text)
117 condition = Condition(_preceding_text)
118 _preceding_text_cache[pattern] = condition
118 _preceding_text_cache[pattern] = condition
119 return condition
119 return condition
120
120
121
121
122 def following_text(pattern):
122 def following_text(pattern):
123 try:
123 try:
124 return _following_text_cache[pattern]
124 return _following_text_cache[pattern]
125 except KeyError:
125 except KeyError:
126 pass
126 pass
127 m = re.compile(pattern)
127 m = re.compile(pattern)
128
128
129 def _following_text():
129 def _following_text():
130 app = get_app()
130 app = get_app()
131 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
131 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
132
132
133 _following_text.__name__ = f"following_text({pattern!r})"
133 _following_text.__name__ = f"following_text({pattern!r})"
134
134
135 condition = Condition(_following_text)
135 condition = Condition(_following_text)
136 _following_text_cache[pattern] = condition
136 _following_text_cache[pattern] = condition
137 return condition
137 return condition
138
138
139
139
140 @Condition
140 @Condition
141 def not_inside_unclosed_string():
141 def not_inside_unclosed_string():
142 app = get_app()
142 app = get_app()
143 s = app.current_buffer.document.text_before_cursor
143 s = app.current_buffer.document.text_before_cursor
144 # remove escaped quotes
144 # remove escaped quotes
145 s = s.replace('\\"', "").replace("\\'", "")
145 s = s.replace('\\"', "").replace("\\'", "")
146 # remove triple-quoted string literals
146 # remove triple-quoted string literals
147 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
147 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
148 # remove single-quoted string literals
148 # remove single-quoted string literals
149 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
149 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
150 return not ('"' in s or "'" in s)
150 return not ('"' in s or "'" in s)
151
151
152
152
153 @Condition
153 @Condition
154 def navigable_suggestions():
154 def navigable_suggestions():
155 shell = get_ipython()
155 shell = get_ipython()
156 return isinstance(shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory)
156 return isinstance(shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory)
157
157
158
158
159 @Condition
159 @Condition
160 def readline_like_completions():
160 def readline_like_completions():
161 shell = get_ipython()
161 shell = get_ipython()
162 return shell.display_completions == "readlinelike"
162 return shell.display_completions == "readlinelike"
163
163
164
164
165 @Condition
165 @Condition
166 def is_windows_os():
166 def is_windows_os():
167 return sys.platform == "win32"
167 return sys.platform == "win32"
168
168
169
169
170 # these one is callable and re-used multiple times hence needs to be
170 # these one is callable and re-used multiple times hence needs to be
171 # only defined once beforhand so that transforming back to human-readable
171 # only defined once beforhand so that transforming back to human-readable
172 # names works well in the documentation.
172 # names works well in the documentation.
173 default_buffer_focused = has_focus(DEFAULT_BUFFER)
173 default_buffer_focused = has_focus(DEFAULT_BUFFER)
174
174
175 KEYBINDING_FILTERS = {
175 KEYBINDING_FILTERS = {
176 "always": Always(),
176 "always": Always(),
177 "has_line_below": has_line_below,
177 "has_line_below": has_line_below,
178 "has_line_above": has_line_above,
178 "has_line_above": has_line_above,
179 "has_selection": has_selection,
179 "has_selection": has_selection,
180 "has_suggestion": has_suggestion,
180 "has_suggestion": has_suggestion,
181 "vi_mode": vi_mode,
181 "vi_mode": vi_mode,
182 "vi_insert_mode": vi_insert_mode,
182 "vi_insert_mode": vi_insert_mode,
183 "emacs_insert_mode": emacs_insert_mode,
183 "emacs_insert_mode": emacs_insert_mode,
184 # https://github.com/ipython/ipython/pull/12603 argued for inclusion of
185 # emacs key bindings with a configurable `emacs_bindings_in_vi_insert_mode`
186 # toggle; when the toggle is on user can access keybindigns like `ctrl + e`
187 # in vi insert mode. Because some of the emacs bindings involve `escape`
188 # followed by another key, e.g. `escape` followed by `f`, prompt-toolkit
189 # needs to wait to see if there will be another character typed in before
190 # executing pure `escape` keybinding; in vi insert mode `escape` switches to
191 # command mode which is common and performance critical action for vi users.
192 # To avoid the delay users employ a workaround:
193 # https://github.com/ipython/ipython/issues/13443#issuecomment-1032753703
194 # which involves switching `emacs_bindings_in_vi_insert_mode` off.
195 #
196 # For the workaround to work:
197 # 1) end users need to toggle `emacs_bindings_in_vi_insert_mode` off
198 # 2) all keybindings which would involve `escape` need to respect that
199 # toggle by including either:
200 # - `vi_insert_mode & ebivim` for actions which have emacs keybindings
201 # predefined upstream in prompt-toolkit, or
202 # - `emacs_like_insert_mode` for actions which do not have existing
203 # emacs keybindings predefined upstream (or need overriding of the
204 # upstream bindings to modify behaviour), defined below.
205 "emacs_like_insert_mode": (vi_insert_mode & ebivim) | emacs_insert_mode,
184 "has_completions": has_completions,
206 "has_completions": has_completions,
185 "insert_mode": vi_insert_mode | emacs_insert_mode,
207 "insert_mode": vi_insert_mode | emacs_insert_mode,
186 "default_buffer_focused": default_buffer_focused,
208 "default_buffer_focused": default_buffer_focused,
187 "search_buffer_focused": has_focus(SEARCH_BUFFER),
209 "search_buffer_focused": has_focus(SEARCH_BUFFER),
210 # `ebivim` stands for emacs bindings in vi insert mode
188 "ebivim": ebivim,
211 "ebivim": ebivim,
189 "supports_suspend": supports_suspend,
212 "supports_suspend": supports_suspend,
190 "is_windows_os": is_windows_os,
213 "is_windows_os": is_windows_os,
191 "auto_match": auto_match,
214 "auto_match": auto_match,
192 "focused_insert": (vi_insert_mode | emacs_insert_mode) & default_buffer_focused,
215 "focused_insert": (vi_insert_mode | emacs_insert_mode) & default_buffer_focused,
193 "not_inside_unclosed_string": not_inside_unclosed_string,
216 "not_inside_unclosed_string": not_inside_unclosed_string,
194 "readline_like_completions": readline_like_completions,
217 "readline_like_completions": readline_like_completions,
195 "preceded_by_paired_double_quotes": preceding_text(
218 "preceded_by_paired_double_quotes": preceding_text(
196 lambda line: all_quotes_paired('"', line)
219 lambda line: all_quotes_paired('"', line)
197 ),
220 ),
198 "preceded_by_paired_single_quotes": preceding_text(
221 "preceded_by_paired_single_quotes": preceding_text(
199 lambda line: all_quotes_paired("'", line)
222 lambda line: all_quotes_paired("'", line)
200 ),
223 ),
201 "preceded_by_raw_str_prefix": preceding_text(r".*(r|R)[\"'](-*)$"),
224 "preceded_by_raw_str_prefix": preceding_text(r".*(r|R)[\"'](-*)$"),
202 "preceded_by_two_double_quotes": preceding_text(r'^.*""$'),
225 "preceded_by_two_double_quotes": preceding_text(r'^.*""$'),
203 "preceded_by_two_single_quotes": preceding_text(r"^.*''$"),
226 "preceded_by_two_single_quotes": preceding_text(r"^.*''$"),
204 "followed_by_closing_paren_or_end": following_text(r"[,)}\]]|$"),
227 "followed_by_closing_paren_or_end": following_text(r"[,)}\]]|$"),
205 "preceded_by_opening_round_paren": preceding_text(r".*\($"),
228 "preceded_by_opening_round_paren": preceding_text(r".*\($"),
206 "preceded_by_opening_bracket": preceding_text(r".*\[$"),
229 "preceded_by_opening_bracket": preceding_text(r".*\[$"),
207 "preceded_by_opening_brace": preceding_text(r".*\{$"),
230 "preceded_by_opening_brace": preceding_text(r".*\{$"),
208 "preceded_by_double_quote": preceding_text('.*"$'),
231 "preceded_by_double_quote": preceding_text('.*"$'),
209 "preceded_by_single_quote": preceding_text(r".*'$"),
232 "preceded_by_single_quote": preceding_text(r".*'$"),
210 "followed_by_closing_round_paren": following_text(r"^\)"),
233 "followed_by_closing_round_paren": following_text(r"^\)"),
211 "followed_by_closing_bracket": following_text(r"^\]"),
234 "followed_by_closing_bracket": following_text(r"^\]"),
212 "followed_by_closing_brace": following_text(r"^\}"),
235 "followed_by_closing_brace": following_text(r"^\}"),
213 "followed_by_double_quote": following_text('^"'),
236 "followed_by_double_quote": following_text('^"'),
214 "followed_by_single_quote": following_text("^'"),
237 "followed_by_single_quote": following_text("^'"),
215 "navigable_suggestions": navigable_suggestions,
238 "navigable_suggestions": navigable_suggestions,
216 "cursor_in_leading_ws": cursor_in_leading_ws,
239 "cursor_in_leading_ws": cursor_in_leading_ws,
217 }
240 }
218
241
219
242
220 def eval_node(node: Union[ast.AST, None]):
243 def eval_node(node: Union[ast.AST, None]):
221 if node is None:
244 if node is None:
222 return None
245 return None
223 if isinstance(node, ast.Expression):
246 if isinstance(node, ast.Expression):
224 return eval_node(node.body)
247 return eval_node(node.body)
225 if isinstance(node, ast.BinOp):
248 if isinstance(node, ast.BinOp):
226 left = eval_node(node.left)
249 left = eval_node(node.left)
227 right = eval_node(node.right)
250 right = eval_node(node.right)
228 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
251 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
229 if dunders:
252 if dunders:
230 return getattr(left, dunders[0])(right)
253 return getattr(left, dunders[0])(right)
231 raise ValueError(f"Unknown binary operation: {node.op}")
254 raise ValueError(f"Unknown binary operation: {node.op}")
232 if isinstance(node, ast.UnaryOp):
255 if isinstance(node, ast.UnaryOp):
233 value = eval_node(node.operand)
256 value = eval_node(node.operand)
234 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
257 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
235 if dunders:
258 if dunders:
236 return getattr(value, dunders[0])()
259 return getattr(value, dunders[0])()
237 raise ValueError(f"Unknown unary operation: {node.op}")
260 raise ValueError(f"Unknown unary operation: {node.op}")
238 if isinstance(node, ast.Name):
261 if isinstance(node, ast.Name):
239 if node.id in KEYBINDING_FILTERS:
262 if node.id in KEYBINDING_FILTERS:
240 return KEYBINDING_FILTERS[node.id]
263 return KEYBINDING_FILTERS[node.id]
241 else:
264 else:
242 sep = "\n - "
265 sep = "\n - "
243 known_filters = sep.join(sorted(KEYBINDING_FILTERS))
266 known_filters = sep.join(sorted(KEYBINDING_FILTERS))
244 raise NameError(
267 raise NameError(
245 f"{node.id} is not a known shortcut filter."
268 f"{node.id} is not a known shortcut filter."
246 f" Known filters are: {sep}{known_filters}."
269 f" Known filters are: {sep}{known_filters}."
247 )
270 )
248 raise ValueError("Unhandled node", ast.dump(node))
271 raise ValueError("Unhandled node", ast.dump(node))
249
272
250
273
251 def filter_from_string(code: str):
274 def filter_from_string(code: str):
252 expression = ast.parse(code, mode="eval")
275 expression = ast.parse(code, mode="eval")
253 return eval_node(expression)
276 return eval_node(expression)
254
277
255
278
256 __all__ = ["KEYBINDING_FILTERS", "filter_from_string"]
279 __all__ = ["KEYBINDING_FILTERS", "filter_from_string"]
General Comments 0
You need to be logged in to leave comments. Login now