##// END OF EJS Templates
Use module level getattr to emit deprecation
Matthias Bussonnier -
Show More
@@ -1,617 +1,617 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:
184 # there are two reasons for re-defining bindings defined upstream:
185 # 1) prompt-toolkit does not execute autosuggestion bindings in vi mode,
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
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
187 # hence it does not work in multi-line mode of navigable provider
188 Binding(
188 Binding(
189 auto_suggest.accept_in_vi_insert_mode,
189 auto_suggest.accept_or_jump_to_end,
190 ["end"],
190 ["end"],
191 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
191 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
192 ),
192 ),
193 Binding(
193 Binding(
194 auto_suggest.accept_in_vi_insert_mode,
194 auto_suggest.accept_or_jump_to_end,
195 ["c-e"],
195 ["c-e"],
196 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
196 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
197 ),
197 ),
198 Binding(
198 Binding(
199 auto_suggest.accept,
199 auto_suggest.accept,
200 ["c-f"],
200 ["c-f"],
201 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
201 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
202 ),
202 ),
203 Binding(
203 Binding(
204 auto_suggest.accept,
204 auto_suggest.accept,
205 ["right"],
205 ["right"],
206 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
206 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
207 ),
207 ),
208 Binding(
208 Binding(
209 auto_suggest.accept_word,
209 auto_suggest.accept_word,
210 ["escape", "f"],
210 ["escape", "f"],
211 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
211 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
212 ),
212 ),
213 Binding(
213 Binding(
214 auto_suggest.accept_token,
214 auto_suggest.accept_token,
215 ["c-right"],
215 ["c-right"],
216 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
216 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
217 ),
217 ),
218 Binding(
218 Binding(
219 auto_suggest.discard,
219 auto_suggest.discard,
220 ["escape"],
220 ["escape"],
221 # note this one is using `emacs_insert_mode`, not `emacs_like_insert_mode`
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).
222 # as in `vi_insert_mode` we do not want `escape` to be shadowed (ever).
223 "has_suggestion & default_buffer_focused & emacs_insert_mode",
223 "has_suggestion & default_buffer_focused & emacs_insert_mode",
224 ),
224 ),
225 Binding(
225 Binding(
226 auto_suggest.swap_autosuggestion_up,
226 auto_suggest.swap_autosuggestion_up,
227 ["up"],
227 ["up"],
228 "navigable_suggestions"
228 "navigable_suggestions"
229 " & ~has_line_above"
229 " & ~has_line_above"
230 " & has_suggestion"
230 " & has_suggestion"
231 " & default_buffer_focused",
231 " & default_buffer_focused",
232 ),
232 ),
233 Binding(
233 Binding(
234 auto_suggest.swap_autosuggestion_down,
234 auto_suggest.swap_autosuggestion_down,
235 ["down"],
235 ["down"],
236 "navigable_suggestions"
236 "navigable_suggestions"
237 " & ~has_line_below"
237 " & ~has_line_below"
238 " & has_suggestion"
238 " & has_suggestion"
239 " & default_buffer_focused",
239 " & default_buffer_focused",
240 ),
240 ),
241 Binding(
241 Binding(
242 auto_suggest.up_and_update_hint,
242 auto_suggest.up_and_update_hint,
243 ["up"],
243 ["up"],
244 "has_line_above & navigable_suggestions & default_buffer_focused",
244 "has_line_above & navigable_suggestions & default_buffer_focused",
245 ),
245 ),
246 Binding(
246 Binding(
247 auto_suggest.down_and_update_hint,
247 auto_suggest.down_and_update_hint,
248 ["down"],
248 ["down"],
249 "has_line_below & navigable_suggestions & default_buffer_focused",
249 "has_line_below & navigable_suggestions & default_buffer_focused",
250 ),
250 ),
251 Binding(
251 Binding(
252 auto_suggest.accept_character,
252 auto_suggest.accept_character,
253 ["escape", "right"],
253 ["escape", "right"],
254 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
254 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
255 ),
255 ),
256 Binding(
256 Binding(
257 auto_suggest.accept_and_move_cursor_left,
257 auto_suggest.accept_and_move_cursor_left,
258 ["c-left"],
258 ["c-left"],
259 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
259 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
260 ),
260 ),
261 Binding(
261 Binding(
262 auto_suggest.accept_and_keep_cursor,
262 auto_suggest.accept_and_keep_cursor,
263 ["c-down"],
263 ["c-down"],
264 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
264 "has_suggestion & default_buffer_focused & emacs_like_insert_mode",
265 ),
265 ),
266 Binding(
266 Binding(
267 auto_suggest.backspace_and_resume_hint,
267 auto_suggest.backspace_and_resume_hint,
268 ["backspace"],
268 ["backspace"],
269 # no `has_suggestion` here to allow resuming if no suggestion
269 # no `has_suggestion` here to allow resuming if no suggestion
270 "default_buffer_focused & emacs_like_insert_mode",
270 "default_buffer_focused & emacs_like_insert_mode",
271 ),
271 ),
272 ]
272 ]
273
273
274
274
275 SIMPLE_CONTROL_BINDINGS = [
275 SIMPLE_CONTROL_BINDINGS = [
276 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
276 Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
277 for key, cmd in {
277 for key, cmd in {
278 "c-a": nc.beginning_of_line,
278 "c-a": nc.beginning_of_line,
279 "c-b": nc.backward_char,
279 "c-b": nc.backward_char,
280 "c-k": nc.kill_line,
280 "c-k": nc.kill_line,
281 "c-w": nc.backward_kill_word,
281 "c-w": nc.backward_kill_word,
282 "c-y": nc.yank,
282 "c-y": nc.yank,
283 "c-_": nc.undo,
283 "c-_": nc.undo,
284 }.items()
284 }.items()
285 ]
285 ]
286
286
287
287
288 ALT_AND_COMOBO_CONTROL_BINDINGS = [
288 ALT_AND_COMOBO_CONTROL_BINDINGS = [
289 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
289 Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
290 for keys, cmd in {
290 for keys, cmd in {
291 # Control Combos
291 # Control Combos
292 ("c-x", "c-e"): nc.edit_and_execute,
292 ("c-x", "c-e"): nc.edit_and_execute,
293 ("c-x", "e"): nc.edit_and_execute,
293 ("c-x", "e"): nc.edit_and_execute,
294 # Alt
294 # Alt
295 ("escape", "b"): nc.backward_word,
295 ("escape", "b"): nc.backward_word,
296 ("escape", "c"): nc.capitalize_word,
296 ("escape", "c"): nc.capitalize_word,
297 ("escape", "d"): nc.kill_word,
297 ("escape", "d"): nc.kill_word,
298 ("escape", "h"): nc.backward_kill_word,
298 ("escape", "h"): nc.backward_kill_word,
299 ("escape", "l"): nc.downcase_word,
299 ("escape", "l"): nc.downcase_word,
300 ("escape", "u"): nc.uppercase_word,
300 ("escape", "u"): nc.uppercase_word,
301 ("escape", "y"): nc.yank_pop,
301 ("escape", "y"): nc.yank_pop,
302 ("escape", "."): nc.yank_last_arg,
302 ("escape", "."): nc.yank_last_arg,
303 }.items()
303 }.items()
304 ]
304 ]
305
305
306
306
307 def add_binding(bindings: KeyBindings, binding: Binding):
307 def add_binding(bindings: KeyBindings, binding: Binding):
308 bindings.add(
308 bindings.add(
309 *binding.keys,
309 *binding.keys,
310 **({"filter": binding.filter} if binding.filter is not None else {}),
310 **({"filter": binding.filter} if binding.filter is not None else {}),
311 )(binding.command)
311 )(binding.command)
312
312
313
313
314 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
314 def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
315 """Set up the prompt_toolkit keyboard shortcuts for IPython.
315 """Set up the prompt_toolkit keyboard shortcuts for IPython.
316
316
317 Parameters
317 Parameters
318 ----------
318 ----------
319 shell: InteractiveShell
319 shell: InteractiveShell
320 The current IPython shell Instance
320 The current IPython shell Instance
321 skip: List[Binding]
321 skip: List[Binding]
322 Bindings to skip.
322 Bindings to skip.
323
323
324 Returns
324 Returns
325 -------
325 -------
326 KeyBindings
326 KeyBindings
327 the keybinding instance for prompt toolkit.
327 the keybinding instance for prompt toolkit.
328
328
329 """
329 """
330 kb = KeyBindings()
330 kb = KeyBindings()
331 skip = skip or []
331 skip = skip or []
332 for binding in KEY_BINDINGS:
332 for binding in KEY_BINDINGS:
333 skip_this_one = False
333 skip_this_one = False
334 for to_skip in skip:
334 for to_skip in skip:
335 if (
335 if (
336 to_skip.command == binding.command
336 to_skip.command == binding.command
337 and to_skip.filter == binding.filter
337 and to_skip.filter == binding.filter
338 and to_skip.keys == binding.keys
338 and to_skip.keys == binding.keys
339 ):
339 ):
340 skip_this_one = True
340 skip_this_one = True
341 break
341 break
342 if skip_this_one:
342 if skip_this_one:
343 continue
343 continue
344 add_binding(kb, binding)
344 add_binding(kb, binding)
345
345
346 def get_input_mode(self):
346 def get_input_mode(self):
347 app = get_app()
347 app = get_app()
348 app.ttimeoutlen = shell.ttimeoutlen
348 app.ttimeoutlen = shell.ttimeoutlen
349 app.timeoutlen = shell.timeoutlen
349 app.timeoutlen = shell.timeoutlen
350
350
351 return self._input_mode
351 return self._input_mode
352
352
353 def set_input_mode(self, mode):
353 def set_input_mode(self, mode):
354 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
354 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
355 cursor = "\x1b[{} q".format(shape)
355 cursor = "\x1b[{} q".format(shape)
356
356
357 sys.stdout.write(cursor)
357 sys.stdout.write(cursor)
358 sys.stdout.flush()
358 sys.stdout.flush()
359
359
360 self._input_mode = mode
360 self._input_mode = mode
361
361
362 if shell.editing_mode == "vi" and shell.modal_cursor:
362 if shell.editing_mode == "vi" and shell.modal_cursor:
363 ViState._input_mode = InputMode.INSERT # type: ignore
363 ViState._input_mode = InputMode.INSERT # type: ignore
364 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
364 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
365
365
366 return kb
366 return kb
367
367
368
368
369 def reformat_and_execute(event):
369 def reformat_and_execute(event):
370 """Reformat code and execute it"""
370 """Reformat code and execute it"""
371 shell = get_ipython()
371 shell = get_ipython()
372 reformat_text_before_cursor(
372 reformat_text_before_cursor(
373 event.current_buffer, event.current_buffer.document, shell
373 event.current_buffer, event.current_buffer.document, shell
374 )
374 )
375 event.current_buffer.validate_and_handle()
375 event.current_buffer.validate_and_handle()
376
376
377
377
378 def reformat_text_before_cursor(buffer, document, shell):
378 def reformat_text_before_cursor(buffer, document, shell):
379 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
379 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
380 try:
380 try:
381 formatted_text = shell.reformat_handler(text)
381 formatted_text = shell.reformat_handler(text)
382 buffer.insert_text(formatted_text)
382 buffer.insert_text(formatted_text)
383 except Exception as e:
383 except Exception as e:
384 buffer.insert_text(text)
384 buffer.insert_text(text)
385
385
386
386
387 def handle_return_or_newline_or_execute(event):
387 def handle_return_or_newline_or_execute(event):
388 shell = get_ipython()
388 shell = get_ipython()
389 if getattr(shell, "handle_return", None):
389 if getattr(shell, "handle_return", None):
390 return shell.handle_return(shell)(event)
390 return shell.handle_return(shell)(event)
391 else:
391 else:
392 return newline_or_execute_outer(shell)(event)
392 return newline_or_execute_outer(shell)(event)
393
393
394
394
395 def newline_or_execute_outer(shell):
395 def newline_or_execute_outer(shell):
396 def newline_or_execute(event):
396 def newline_or_execute(event):
397 """When the user presses return, insert a newline or execute the code."""
397 """When the user presses return, insert a newline or execute the code."""
398 b = event.current_buffer
398 b = event.current_buffer
399 d = b.document
399 d = b.document
400
400
401 if b.complete_state:
401 if b.complete_state:
402 cc = b.complete_state.current_completion
402 cc = b.complete_state.current_completion
403 if cc:
403 if cc:
404 b.apply_completion(cc)
404 b.apply_completion(cc)
405 else:
405 else:
406 b.cancel_completion()
406 b.cancel_completion()
407 return
407 return
408
408
409 # If there's only one line, treat it as if the cursor is at the end.
409 # If there's only one line, treat it as if the cursor is at the end.
410 # See https://github.com/ipython/ipython/issues/10425
410 # See https://github.com/ipython/ipython/issues/10425
411 if d.line_count == 1:
411 if d.line_count == 1:
412 check_text = d.text
412 check_text = d.text
413 else:
413 else:
414 check_text = d.text[: d.cursor_position]
414 check_text = d.text[: d.cursor_position]
415 status, indent = shell.check_complete(check_text)
415 status, indent = shell.check_complete(check_text)
416
416
417 # if all we have after the cursor is whitespace: reformat current text
417 # if all we have after the cursor is whitespace: reformat current text
418 # before cursor
418 # before cursor
419 after_cursor = d.text[d.cursor_position :]
419 after_cursor = d.text[d.cursor_position :]
420 reformatted = False
420 reformatted = False
421 if not after_cursor.strip():
421 if not after_cursor.strip():
422 reformat_text_before_cursor(b, d, shell)
422 reformat_text_before_cursor(b, d, shell)
423 reformatted = True
423 reformatted = True
424 if not (
424 if not (
425 d.on_last_line
425 d.on_last_line
426 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
426 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
427 ):
427 ):
428 if shell.autoindent:
428 if shell.autoindent:
429 b.insert_text("\n" + indent)
429 b.insert_text("\n" + indent)
430 else:
430 else:
431 b.insert_text("\n")
431 b.insert_text("\n")
432 return
432 return
433
433
434 if (status != "incomplete") and b.accept_handler:
434 if (status != "incomplete") and b.accept_handler:
435 if not reformatted:
435 if not reformatted:
436 reformat_text_before_cursor(b, d, shell)
436 reformat_text_before_cursor(b, d, shell)
437 b.validate_and_handle()
437 b.validate_and_handle()
438 else:
438 else:
439 if shell.autoindent:
439 if shell.autoindent:
440 b.insert_text("\n" + indent)
440 b.insert_text("\n" + indent)
441 else:
441 else:
442 b.insert_text("\n")
442 b.insert_text("\n")
443
443
444 return newline_or_execute
444 return newline_or_execute
445
445
446
446
447 def previous_history_or_previous_completion(event):
447 def previous_history_or_previous_completion(event):
448 """
448 """
449 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
449 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
450
450
451 If completer is open this still select previous completion.
451 If completer is open this still select previous completion.
452 """
452 """
453 event.current_buffer.auto_up()
453 event.current_buffer.auto_up()
454
454
455
455
456 def next_history_or_next_completion(event):
456 def next_history_or_next_completion(event):
457 """
457 """
458 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
458 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
459
459
460 If completer is open this still select next completion.
460 If completer is open this still select next completion.
461 """
461 """
462 event.current_buffer.auto_down()
462 event.current_buffer.auto_down()
463
463
464
464
465 def dismiss_completion(event):
465 def dismiss_completion(event):
466 """Dismiss completion"""
466 """Dismiss completion"""
467 b = event.current_buffer
467 b = event.current_buffer
468 if b.complete_state:
468 if b.complete_state:
469 b.cancel_completion()
469 b.cancel_completion()
470
470
471
471
472 def reset_buffer(event):
472 def reset_buffer(event):
473 """Reset buffer"""
473 """Reset buffer"""
474 b = event.current_buffer
474 b = event.current_buffer
475 if b.complete_state:
475 if b.complete_state:
476 b.cancel_completion()
476 b.cancel_completion()
477 else:
477 else:
478 b.reset()
478 b.reset()
479
479
480
480
481 def reset_search_buffer(event):
481 def reset_search_buffer(event):
482 """Reset search buffer"""
482 """Reset search buffer"""
483 if event.current_buffer.document.text:
483 if event.current_buffer.document.text:
484 event.current_buffer.reset()
484 event.current_buffer.reset()
485 else:
485 else:
486 event.app.layout.focus(DEFAULT_BUFFER)
486 event.app.layout.focus(DEFAULT_BUFFER)
487
487
488
488
489 def suspend_to_bg(event):
489 def suspend_to_bg(event):
490 """Suspend to background"""
490 """Suspend to background"""
491 event.app.suspend_to_background()
491 event.app.suspend_to_background()
492
492
493
493
494 def quit(event):
494 def quit(event):
495 """
495 """
496 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
496 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
497
497
498 On platforms that support SIGQUIT, send SIGQUIT to the current process.
498 On platforms that support SIGQUIT, send SIGQUIT to the current process.
499 On other platforms, just exit the process with a message.
499 On other platforms, just exit the process with a message.
500 """
500 """
501 sigquit = getattr(signal, "SIGQUIT", None)
501 sigquit = getattr(signal, "SIGQUIT", None)
502 if sigquit is not None:
502 if sigquit is not None:
503 os.kill(0, signal.SIGQUIT)
503 os.kill(0, signal.SIGQUIT)
504 else:
504 else:
505 sys.exit("Quit")
505 sys.exit("Quit")
506
506
507
507
508 def indent_buffer(event):
508 def indent_buffer(event):
509 """Indent buffer"""
509 """Indent buffer"""
510 event.current_buffer.insert_text(" " * 4)
510 event.current_buffer.insert_text(" " * 4)
511
511
512
512
513 def newline_autoindent(event):
513 def newline_autoindent(event):
514 """Insert a newline after the cursor indented appropriately.
514 """Insert a newline after the cursor indented appropriately.
515
515
516 Fancier version of former ``newline_with_copy_margin`` which should
516 Fancier version of former ``newline_with_copy_margin`` which should
517 compute the correct indentation of the inserted line. That is to say, indent
517 compute the correct indentation of the inserted line. That is to say, indent
518 by 4 extra space after a function definition, class definition, context
518 by 4 extra space after a function definition, class definition, context
519 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
519 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
520 """
520 """
521 shell = get_ipython()
521 shell = get_ipython()
522 inputsplitter = shell.input_transformer_manager
522 inputsplitter = shell.input_transformer_manager
523 b = event.current_buffer
523 b = event.current_buffer
524 d = b.document
524 d = b.document
525
525
526 if b.complete_state:
526 if b.complete_state:
527 b.cancel_completion()
527 b.cancel_completion()
528 text = d.text[: d.cursor_position] + "\n"
528 text = d.text[: d.cursor_position] + "\n"
529 _, indent = inputsplitter.check_complete(text)
529 _, indent = inputsplitter.check_complete(text)
530 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
530 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
531
531
532
532
533 def open_input_in_editor(event):
533 def open_input_in_editor(event):
534 """Open code from input in external editor"""
534 """Open code from input in external editor"""
535 event.app.current_buffer.open_in_editor()
535 event.app.current_buffer.open_in_editor()
536
536
537
537
538 if sys.platform == "win32":
538 if sys.platform == "win32":
539 from IPython.core.error import TryNext
539 from IPython.core.error import TryNext
540 from IPython.lib.clipboard import (
540 from IPython.lib.clipboard import (
541 ClipboardEmpty,
541 ClipboardEmpty,
542 tkinter_clipboard_get,
542 tkinter_clipboard_get,
543 win32_clipboard_get,
543 win32_clipboard_get,
544 )
544 )
545
545
546 @undoc
546 @undoc
547 def win_paste(event):
547 def win_paste(event):
548 try:
548 try:
549 text = win32_clipboard_get()
549 text = win32_clipboard_get()
550 except TryNext:
550 except TryNext:
551 try:
551 try:
552 text = tkinter_clipboard_get()
552 text = tkinter_clipboard_get()
553 except (TryNext, ClipboardEmpty):
553 except (TryNext, ClipboardEmpty):
554 return
554 return
555 except ClipboardEmpty:
555 except ClipboardEmpty:
556 return
556 return
557 event.current_buffer.insert_text(text.replace("\t", " " * 4))
557 event.current_buffer.insert_text(text.replace("\t", " " * 4))
558
558
559 else:
559 else:
560
560
561 @undoc
561 @undoc
562 def win_paste(event):
562 def win_paste(event):
563 """Stub used on other platforms"""
563 """Stub used on other platforms"""
564 pass
564 pass
565
565
566
566
567 KEY_BINDINGS = [
567 KEY_BINDINGS = [
568 Binding(
568 Binding(
569 handle_return_or_newline_or_execute,
569 handle_return_or_newline_or_execute,
570 ["enter"],
570 ["enter"],
571 "default_buffer_focused & ~has_selection & insert_mode",
571 "default_buffer_focused & ~has_selection & insert_mode",
572 ),
572 ),
573 Binding(
573 Binding(
574 reformat_and_execute,
574 reformat_and_execute,
575 ["escape", "enter"],
575 ["escape", "enter"],
576 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
576 "default_buffer_focused & ~has_selection & insert_mode & ebivim",
577 ),
577 ),
578 Binding(quit, ["c-\\"]),
578 Binding(quit, ["c-\\"]),
579 Binding(
579 Binding(
580 previous_history_or_previous_completion,
580 previous_history_or_previous_completion,
581 ["c-p"],
581 ["c-p"],
582 "vi_insert_mode & default_buffer_focused",
582 "vi_insert_mode & default_buffer_focused",
583 ),
583 ),
584 Binding(
584 Binding(
585 next_history_or_next_completion,
585 next_history_or_next_completion,
586 ["c-n"],
586 ["c-n"],
587 "vi_insert_mode & default_buffer_focused",
587 "vi_insert_mode & default_buffer_focused",
588 ),
588 ),
589 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
589 Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
590 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
590 Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
591 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
591 Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
592 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
592 Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
593 Binding(
593 Binding(
594 indent_buffer,
594 indent_buffer,
595 ["tab"], # Ctrl+I == Tab
595 ["tab"], # Ctrl+I == Tab
596 "default_buffer_focused"
596 "default_buffer_focused"
597 " & ~has_selection"
597 " & ~has_selection"
598 " & insert_mode"
598 " & insert_mode"
599 " & cursor_in_leading_ws",
599 " & cursor_in_leading_ws",
600 ),
600 ),
601 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
601 Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
602 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
602 Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
603 *AUTO_MATCH_BINDINGS,
603 *AUTO_MATCH_BINDINGS,
604 *AUTO_SUGGEST_BINDINGS,
604 *AUTO_SUGGEST_BINDINGS,
605 Binding(
605 Binding(
606 display_completions_like_readline,
606 display_completions_like_readline,
607 ["c-i"],
607 ["c-i"],
608 "readline_like_completions"
608 "readline_like_completions"
609 " & default_buffer_focused"
609 " & default_buffer_focused"
610 " & ~has_selection"
610 " & ~has_selection"
611 " & insert_mode"
611 " & insert_mode"
612 " & ~cursor_in_leading_ws",
612 " & ~cursor_in_leading_ws",
613 ),
613 ),
614 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
614 Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
615 *SIMPLE_CONTROL_BINDINGS,
615 *SIMPLE_CONTROL_BINDINGS,
616 *ALT_AND_COMOBO_CONTROL_BINDINGS,
616 *ALT_AND_COMOBO_CONTROL_BINDINGS,
617 ]
617 ]
@@ -1,383 +1,397 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 import warnings
5
6
6 from prompt_toolkit.buffer import Buffer
7 from prompt_toolkit.buffer import Buffer
7 from prompt_toolkit.key_binding import KeyPressEvent
8 from prompt_toolkit.key_binding import KeyPressEvent
8 from prompt_toolkit.key_binding.bindings import named_commands as nc
9 from prompt_toolkit.key_binding.bindings import named_commands as nc
9 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
10 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
10 from prompt_toolkit.document import Document
11 from prompt_toolkit.document import Document
11 from prompt_toolkit.history import History
12 from prompt_toolkit.history import History
12 from prompt_toolkit.shortcuts import PromptSession
13 from prompt_toolkit.shortcuts import PromptSession
13 from prompt_toolkit.layout.processors import (
14 from prompt_toolkit.layout.processors import (
14 Processor,
15 Processor,
15 Transformation,
16 Transformation,
16 TransformationInput,
17 TransformationInput,
17 )
18 )
18
19
19 from IPython.core.getipython import get_ipython
20 from IPython.core.getipython import get_ipython
20 from IPython.utils.tokenutil import generate_tokens
21 from IPython.utils.tokenutil import generate_tokens
21
22
22
23
23 def _get_query(document: Document):
24 def _get_query(document: Document):
24 return document.lines[document.cursor_position_row]
25 return document.lines[document.cursor_position_row]
25
26
26
27
27 class AppendAutoSuggestionInAnyLine(Processor):
28 class AppendAutoSuggestionInAnyLine(Processor):
28 """
29 """
29 Append the auto suggestion to lines other than the last (appending to the
30 Append the auto suggestion to lines other than the last (appending to the
30 last line is natively supported by the prompt toolkit).
31 last line is natively supported by the prompt toolkit).
31 """
32 """
32
33
33 def __init__(self, style: str = "class:auto-suggestion") -> None:
34 def __init__(self, style: str = "class:auto-suggestion") -> None:
34 self.style = style
35 self.style = style
35
36
36 def apply_transformation(self, ti: TransformationInput) -> Transformation:
37 def apply_transformation(self, ti: TransformationInput) -> Transformation:
37 is_last_line = ti.lineno == ti.document.line_count - 1
38 is_last_line = ti.lineno == ti.document.line_count - 1
38 is_active_line = ti.lineno == ti.document.cursor_position_row
39 is_active_line = ti.lineno == ti.document.cursor_position_row
39
40
40 if not is_last_line and is_active_line:
41 if not is_last_line and is_active_line:
41 buffer = ti.buffer_control.buffer
42 buffer = ti.buffer_control.buffer
42
43
43 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
44 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
44 suggestion = buffer.suggestion.text
45 suggestion = buffer.suggestion.text
45 else:
46 else:
46 suggestion = ""
47 suggestion = ""
47
48
48 return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
49 return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
49 else:
50 else:
50 return Transformation(fragments=ti.fragments)
51 return Transformation(fragments=ti.fragments)
51
52
52
53
53 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
54 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
54 """
55 """
55 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
56 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
56 suggestion from history. To do so it remembers the current position, but it
57 suggestion from history. To do so it remembers the current position, but it
57 state need to carefully be cleared on the right events.
58 state need to carefully be cleared on the right events.
58 """
59 """
59
60
60 def __init__(
61 def __init__(
61 self,
62 self,
62 ):
63 ):
63 self.skip_lines = 0
64 self.skip_lines = 0
64 self._connected_apps = []
65 self._connected_apps = []
65
66
66 def reset_history_position(self, _: Buffer):
67 def reset_history_position(self, _: Buffer):
67 self.skip_lines = 0
68 self.skip_lines = 0
68
69
69 def disconnect(self):
70 def disconnect(self):
70 for pt_app in self._connected_apps:
71 for pt_app in self._connected_apps:
71 text_insert_event = pt_app.default_buffer.on_text_insert
72 text_insert_event = pt_app.default_buffer.on_text_insert
72 text_insert_event.remove_handler(self.reset_history_position)
73 text_insert_event.remove_handler(self.reset_history_position)
73
74
74 def connect(self, pt_app: PromptSession):
75 def connect(self, pt_app: PromptSession):
75 self._connected_apps.append(pt_app)
76 self._connected_apps.append(pt_app)
76 # note: `on_text_changed` could be used for a bit different behaviour
77 # note: `on_text_changed` could be used for a bit different behaviour
77 # on character deletion (i.e. reseting history position on backspace)
78 # on character deletion (i.e. reseting history position on backspace)
78 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
79 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)
80 pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
80
81
81 def get_suggestion(
82 def get_suggestion(
82 self, buffer: Buffer, document: Document
83 self, buffer: Buffer, document: Document
83 ) -> Optional[Suggestion]:
84 ) -> Optional[Suggestion]:
84 text = _get_query(document)
85 text = _get_query(document)
85
86
86 if text.strip():
87 if text.strip():
87 for suggestion, _ in self._find_next_match(
88 for suggestion, _ in self._find_next_match(
88 text, self.skip_lines, buffer.history
89 text, self.skip_lines, buffer.history
89 ):
90 ):
90 return Suggestion(suggestion)
91 return Suggestion(suggestion)
91
92
92 return None
93 return None
93
94
94 def _dismiss(self, buffer, *args, **kwargs):
95 def _dismiss(self, buffer, *args, **kwargs):
95 buffer.suggestion = None
96 buffer.suggestion = None
96
97
97 def _find_match(
98 def _find_match(
98 self, text: str, skip_lines: float, history: History, previous: bool
99 self, text: str, skip_lines: float, history: History, previous: bool
99 ) -> Generator[Tuple[str, float], None, None]:
100 ) -> Generator[Tuple[str, float], None, None]:
100 """
101 """
101 text : str
102 text : str
102 Text content to find a match for, the user cursor is most of the
103 Text content to find a match for, the user cursor is most of the
103 time at the end of this text.
104 time at the end of this text.
104 skip_lines : float
105 skip_lines : float
105 number of items to skip in the search, this is used to indicate how
106 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.
107 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
108 The float type is used as the base value is +inf
108 history : History
109 history : History
109 prompt_toolkit History instance to fetch previous entries from.
110 prompt_toolkit History instance to fetch previous entries from.
110 previous : bool
111 previous : bool
111 Direction of the search, whether we are looking previous match
112 Direction of the search, whether we are looking previous match
112 (True), or next match (False).
113 (True), or next match (False).
113
114
114 Yields
115 Yields
115 ------
116 ------
116 Tuple with:
117 Tuple with:
117 str:
118 str:
118 current suggestion.
119 current suggestion.
119 float:
120 float:
120 will actually yield only ints, which is passed back via skip_lines,
121 will actually yield only ints, which is passed back via skip_lines,
121 which may be a +inf (float)
122 which may be a +inf (float)
122
123
123
124
124 """
125 """
125 line_number = -1
126 line_number = -1
126 for string in reversed(list(history.get_strings())):
127 for string in reversed(list(history.get_strings())):
127 for line in reversed(string.splitlines()):
128 for line in reversed(string.splitlines()):
128 line_number += 1
129 line_number += 1
129 if not previous and line_number < skip_lines:
130 if not previous and line_number < skip_lines:
130 continue
131 continue
131 # do not return empty suggestions as these
132 # do not return empty suggestions as these
132 # close the auto-suggestion overlay (and are useless)
133 # close the auto-suggestion overlay (and are useless)
133 if line.startswith(text) and len(line) > len(text):
134 if line.startswith(text) and len(line) > len(text):
134 yield line[len(text) :], line_number
135 yield line[len(text) :], line_number
135 if previous and line_number >= skip_lines:
136 if previous and line_number >= skip_lines:
136 return
137 return
137
138
138 def _find_next_match(
139 def _find_next_match(
139 self, text: str, skip_lines: float, history: History
140 self, text: str, skip_lines: float, history: History
140 ) -> Generator[Tuple[str, float], None, None]:
141 ) -> Generator[Tuple[str, float], None, None]:
141 return self._find_match(text, skip_lines, history, previous=False)
142 return self._find_match(text, skip_lines, history, previous=False)
142
143
143 def _find_previous_match(self, text: str, skip_lines: float, history: History):
144 def _find_previous_match(self, text: str, skip_lines: float, history: History):
144 return reversed(
145 return reversed(
145 list(self._find_match(text, skip_lines, history, previous=True))
146 list(self._find_match(text, skip_lines, history, previous=True))
146 )
147 )
147
148
148 def up(self, query: str, other_than: str, history: History) -> None:
149 def up(self, query: str, other_than: str, history: History) -> None:
149 for suggestion, line_number in self._find_next_match(
150 for suggestion, line_number in self._find_next_match(
150 query, self.skip_lines, history
151 query, self.skip_lines, history
151 ):
152 ):
152 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
153 # 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
154 # we want to switch from 'very.b' to 'very.a' because a) if the
154 # suggestion equals current text, prompt-toolkit aborts suggesting
155 # suggestion equals current text, prompt-toolkit aborts suggesting
155 # b) user likely would not be interested in 'very' anyways (they
156 # b) user likely would not be interested in 'very' anyways (they
156 # already typed it).
157 # already typed it).
157 if query + suggestion != other_than:
158 if query + suggestion != other_than:
158 self.skip_lines = line_number
159 self.skip_lines = line_number
159 break
160 break
160 else:
161 else:
161 # no matches found, cycle back to beginning
162 # no matches found, cycle back to beginning
162 self.skip_lines = 0
163 self.skip_lines = 0
163
164
164 def down(self, query: str, other_than: str, history: History) -> None:
165 def down(self, query: str, other_than: str, history: History) -> None:
165 for suggestion, line_number in self._find_previous_match(
166 for suggestion, line_number in self._find_previous_match(
166 query, self.skip_lines, history
167 query, self.skip_lines, history
167 ):
168 ):
168 if query + suggestion != other_than:
169 if query + suggestion != other_than:
169 self.skip_lines = line_number
170 self.skip_lines = line_number
170 break
171 break
171 else:
172 else:
172 # no matches found, cycle to end
173 # no matches found, cycle to end
173 for suggestion, line_number in self._find_previous_match(
174 for suggestion, line_number in self._find_previous_match(
174 query, float("Inf"), history
175 query, float("Inf"), history
175 ):
176 ):
176 if query + suggestion != other_than:
177 if query + suggestion != other_than:
177 self.skip_lines = line_number
178 self.skip_lines = line_number
178 break
179 break
179
180
180
181
181 def accept_or_jump_to_end(event: KeyPressEvent):
182 def accept_or_jump_to_end(event: KeyPressEvent):
182 """Apply autosuggestion or jump to end of line."""
183 """Apply autosuggestion or jump to end of line."""
183 buffer = event.current_buffer
184 buffer = event.current_buffer
184 d = buffer.document
185 d = buffer.document
185 after_cursor = d.text[d.cursor_position :]
186 after_cursor = d.text[d.cursor_position :]
186 lines = after_cursor.split("\n")
187 lines = after_cursor.split("\n")
187 end_of_current_line = lines[0].strip()
188 end_of_current_line = lines[0].strip()
188 suggestion = buffer.suggestion
189 suggestion = buffer.suggestion
189 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
190 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
190 buffer.insert_text(suggestion.text)
191 buffer.insert_text(suggestion.text)
191 else:
192 else:
192 nc.end_of_line(event)
193 nc.end_of_line(event)
193
194
194
195
195 def accept_in_vi_insert_mode(event: KeyPressEvent):
196 def _deprected_accept_in_vi_insert_mode(event: KeyPressEvent):
196 """Accept autosuggestion or jump to end of line.
197 """Accept autosuggestion or jump to end of line.
197
198
198 .. deprecated:: 8.12
199 .. deprecated:: 8.12
199 Use `accept_or_jump_to_end` instead.
200 Use `accept_or_jump_to_end` instead.
200 """
201 """
201 return accept_or_jump_to_end(event)
202 return accept_or_jump_to_end(event)
202
203
203
204
204 def accept(event: KeyPressEvent):
205 def accept(event: KeyPressEvent):
205 """Accept autosuggestion"""
206 """Accept autosuggestion"""
206 buffer = event.current_buffer
207 buffer = event.current_buffer
207 suggestion = buffer.suggestion
208 suggestion = buffer.suggestion
208 if suggestion:
209 if suggestion:
209 buffer.insert_text(suggestion.text)
210 buffer.insert_text(suggestion.text)
210 else:
211 else:
211 nc.forward_char(event)
212 nc.forward_char(event)
212
213
213
214
214 def discard(event: KeyPressEvent):
215 def discard(event: KeyPressEvent):
215 """Discard autosuggestion"""
216 """Discard autosuggestion"""
216 buffer = event.current_buffer
217 buffer = event.current_buffer
217 buffer.suggestion = None
218 buffer.suggestion = None
218
219
219
220
220 def accept_word(event: KeyPressEvent):
221 def accept_word(event: KeyPressEvent):
221 """Fill partial autosuggestion by word"""
222 """Fill partial autosuggestion by word"""
222 buffer = event.current_buffer
223 buffer = event.current_buffer
223 suggestion = buffer.suggestion
224 suggestion = buffer.suggestion
224 if suggestion:
225 if suggestion:
225 t = re.split(r"(\S+\s+)", suggestion.text)
226 t = re.split(r"(\S+\s+)", suggestion.text)
226 buffer.insert_text(next((x for x in t if x), ""))
227 buffer.insert_text(next((x for x in t if x), ""))
227 else:
228 else:
228 nc.forward_word(event)
229 nc.forward_word(event)
229
230
230
231
231 def accept_character(event: KeyPressEvent):
232 def accept_character(event: KeyPressEvent):
232 """Fill partial autosuggestion by character"""
233 """Fill partial autosuggestion by character"""
233 b = event.current_buffer
234 b = event.current_buffer
234 suggestion = b.suggestion
235 suggestion = b.suggestion
235 if suggestion and suggestion.text:
236 if suggestion and suggestion.text:
236 b.insert_text(suggestion.text[0])
237 b.insert_text(suggestion.text[0])
237
238
238
239
239 def accept_and_keep_cursor(event: KeyPressEvent):
240 def accept_and_keep_cursor(event: KeyPressEvent):
240 """Accept autosuggestion and keep cursor in place"""
241 """Accept autosuggestion and keep cursor in place"""
241 buffer = event.current_buffer
242 buffer = event.current_buffer
242 old_position = buffer.cursor_position
243 old_position = buffer.cursor_position
243 suggestion = buffer.suggestion
244 suggestion = buffer.suggestion
244 if suggestion:
245 if suggestion:
245 buffer.insert_text(suggestion.text)
246 buffer.insert_text(suggestion.text)
246 buffer.cursor_position = old_position
247 buffer.cursor_position = old_position
247
248
248
249
249 def accept_and_move_cursor_left(event: KeyPressEvent):
250 def accept_and_move_cursor_left(event: KeyPressEvent):
250 """Accept autosuggestion and move cursor left in place"""
251 """Accept autosuggestion and move cursor left in place"""
251 accept_and_keep_cursor(event)
252 accept_and_keep_cursor(event)
252 nc.backward_char(event)
253 nc.backward_char(event)
253
254
254
255
255 def _update_hint(buffer: Buffer):
256 def _update_hint(buffer: Buffer):
256 if buffer.auto_suggest:
257 if buffer.auto_suggest:
257 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
258 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
258 buffer.suggestion = suggestion
259 buffer.suggestion = suggestion
259
260
260
261
261 def backspace_and_resume_hint(event: KeyPressEvent):
262 def backspace_and_resume_hint(event: KeyPressEvent):
262 """Resume autosuggestions after deleting last character"""
263 """Resume autosuggestions after deleting last character"""
263 current_buffer = event.current_buffer
264 current_buffer = event.current_buffer
264
265
265 def resume_hinting(buffer: Buffer):
266 def resume_hinting(buffer: Buffer):
266 _update_hint(buffer)
267 _update_hint(buffer)
267 current_buffer.on_text_changed.remove_handler(resume_hinting)
268 current_buffer.on_text_changed.remove_handler(resume_hinting)
268
269
269 current_buffer.on_text_changed.add_handler(resume_hinting)
270 current_buffer.on_text_changed.add_handler(resume_hinting)
270 nc.backward_delete_char(event)
271 nc.backward_delete_char(event)
271
272
272
273
273 def up_and_update_hint(event: KeyPressEvent):
274 def up_and_update_hint(event: KeyPressEvent):
274 """Go up and update hint"""
275 """Go up and update hint"""
275 current_buffer = event.current_buffer
276 current_buffer = event.current_buffer
276
277
277 current_buffer.auto_up(count=event.arg)
278 current_buffer.auto_up(count=event.arg)
278 _update_hint(current_buffer)
279 _update_hint(current_buffer)
279
280
280
281
281 def down_and_update_hint(event: KeyPressEvent):
282 def down_and_update_hint(event: KeyPressEvent):
282 """Go down and update hint"""
283 """Go down and update hint"""
283 current_buffer = event.current_buffer
284 current_buffer = event.current_buffer
284
285
285 current_buffer.auto_down(count=event.arg)
286 current_buffer.auto_down(count=event.arg)
286 _update_hint(current_buffer)
287 _update_hint(current_buffer)
287
288
288
289
289 def accept_token(event: KeyPressEvent):
290 def accept_token(event: KeyPressEvent):
290 """Fill partial autosuggestion by token"""
291 """Fill partial autosuggestion by token"""
291 b = event.current_buffer
292 b = event.current_buffer
292 suggestion = b.suggestion
293 suggestion = b.suggestion
293
294
294 if suggestion:
295 if suggestion:
295 prefix = _get_query(b.document)
296 prefix = _get_query(b.document)
296 text = prefix + suggestion.text
297 text = prefix + suggestion.text
297
298
298 tokens: List[Optional[str]] = [None, None, None]
299 tokens: List[Optional[str]] = [None, None, None]
299 substrings = [""]
300 substrings = [""]
300 i = 0
301 i = 0
301
302
302 for token in generate_tokens(StringIO(text).readline):
303 for token in generate_tokens(StringIO(text).readline):
303 if token.type == tokenize.NEWLINE:
304 if token.type == tokenize.NEWLINE:
304 index = len(text)
305 index = len(text)
305 else:
306 else:
306 index = text.index(token[1], len(substrings[-1]))
307 index = text.index(token[1], len(substrings[-1]))
307 substrings.append(text[:index])
308 substrings.append(text[:index])
308 tokenized_so_far = substrings[-1]
309 tokenized_so_far = substrings[-1]
309 if tokenized_so_far.startswith(prefix):
310 if tokenized_so_far.startswith(prefix):
310 if i == 0 and len(tokenized_so_far) > len(prefix):
311 if i == 0 and len(tokenized_so_far) > len(prefix):
311 tokens[0] = tokenized_so_far[len(prefix) :]
312 tokens[0] = tokenized_so_far[len(prefix) :]
312 substrings.append(tokenized_so_far)
313 substrings.append(tokenized_so_far)
313 i += 1
314 i += 1
314 tokens[i] = token[1]
315 tokens[i] = token[1]
315 if i == 2:
316 if i == 2:
316 break
317 break
317 i += 1
318 i += 1
318
319
319 if tokens[0]:
320 if tokens[0]:
320 to_insert: str
321 to_insert: str
321 insert_text = substrings[-2]
322 insert_text = substrings[-2]
322 if tokens[1] and len(tokens[1]) == 1:
323 if tokens[1] and len(tokens[1]) == 1:
323 insert_text = substrings[-1]
324 insert_text = substrings[-1]
324 to_insert = insert_text[len(prefix) :]
325 to_insert = insert_text[len(prefix) :]
325 b.insert_text(to_insert)
326 b.insert_text(to_insert)
326 return
327 return
327
328
328 nc.forward_word(event)
329 nc.forward_word(event)
329
330
330
331
331 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
332 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
332
333
333
334
334 def _swap_autosuggestion(
335 def _swap_autosuggestion(
335 buffer: Buffer,
336 buffer: Buffer,
336 provider: NavigableAutoSuggestFromHistory,
337 provider: NavigableAutoSuggestFromHistory,
337 direction_method: Callable,
338 direction_method: Callable,
338 ):
339 ):
339 """
340 """
340 We skip most recent history entry (in either direction) if it equals the
341 We skip most recent history entry (in either direction) if it equals the
341 current autosuggestion because if user cycles when auto-suggestion is shown
342 current autosuggestion because if user cycles when auto-suggestion is shown
342 they most likely want something else than what was suggested (otherwise
343 they most likely want something else than what was suggested (otherwise
343 they would have accepted the suggestion).
344 they would have accepted the suggestion).
344 """
345 """
345 suggestion = buffer.suggestion
346 suggestion = buffer.suggestion
346 if not suggestion:
347 if not suggestion:
347 return
348 return
348
349
349 query = _get_query(buffer.document)
350 query = _get_query(buffer.document)
350 current = query + suggestion.text
351 current = query + suggestion.text
351
352
352 direction_method(query=query, other_than=current, history=buffer.history)
353 direction_method(query=query, other_than=current, history=buffer.history)
353
354
354 new_suggestion = provider.get_suggestion(buffer, buffer.document)
355 new_suggestion = provider.get_suggestion(buffer, buffer.document)
355 buffer.suggestion = new_suggestion
356 buffer.suggestion = new_suggestion
356
357
357
358
358 def swap_autosuggestion_up(event: KeyPressEvent):
359 def swap_autosuggestion_up(event: KeyPressEvent):
359 """Get next autosuggestion from history."""
360 """Get next autosuggestion from history."""
360 shell = get_ipython()
361 shell = get_ipython()
361 provider = shell.auto_suggest
362 provider = shell.auto_suggest
362
363
363 if not isinstance(provider, NavigableAutoSuggestFromHistory):
364 if not isinstance(provider, NavigableAutoSuggestFromHistory):
364 return
365 return
365
366
366 return _swap_autosuggestion(
367 return _swap_autosuggestion(
367 buffer=event.current_buffer, provider=provider, direction_method=provider.up
368 buffer=event.current_buffer, provider=provider, direction_method=provider.up
368 )
369 )
369
370
370
371
371 def swap_autosuggestion_down(event: KeyPressEvent):
372 def swap_autosuggestion_down(event: KeyPressEvent):
372 """Get previous autosuggestion from history."""
373 """Get previous autosuggestion from history."""
373 shell = get_ipython()
374 shell = get_ipython()
374 provider = shell.auto_suggest
375 provider = shell.auto_suggest
375
376
376 if not isinstance(provider, NavigableAutoSuggestFromHistory):
377 if not isinstance(provider, NavigableAutoSuggestFromHistory):
377 return
378 return
378
379
379 return _swap_autosuggestion(
380 return _swap_autosuggestion(
380 buffer=event.current_buffer,
381 buffer=event.current_buffer,
381 provider=provider,
382 provider=provider,
382 direction_method=provider.down,
383 direction_method=provider.down,
383 )
384 )
385
386
387 def __getattr__(key):
388 if key == "accept_in_vi_insert_mode":
389 warnings.warn(
390 "`accept_in_vi_insert_mode` is deprecated since IPython 8.12 and "
391 "renamed to `accept_or_jump_to_end`. Please update your configuration "
392 "accordingly",
393 DeprecationWarning,
394 stacklevel=2,
395 )
396 return _deprected_accept_in_vi_insert_mode
397 raise AttributeError
@@ -1,461 +1,468 b''
1 import pytest
1 import pytest
2 from IPython.terminal.shortcuts.auto_suggest import (
2 from IPython.terminal.shortcuts.auto_suggest import (
3 accept,
3 accept,
4 accept_in_vi_insert_mode,
4 accept_or_jump_to_end,
5 accept_token,
5 accept_token,
6 accept_character,
6 accept_character,
7 accept_word,
7 accept_word,
8 accept_and_keep_cursor,
8 accept_and_keep_cursor,
9 discard,
9 discard,
10 NavigableAutoSuggestFromHistory,
10 NavigableAutoSuggestFromHistory,
11 swap_autosuggestion_up,
11 swap_autosuggestion_up,
12 swap_autosuggestion_down,
12 swap_autosuggestion_down,
13 )
13 )
14 from IPython.terminal.shortcuts.auto_match import skip_over
14 from IPython.terminal.shortcuts.auto_match import skip_over
15 from IPython.terminal.shortcuts import create_ipython_shortcuts
15 from IPython.terminal.shortcuts import create_ipython_shortcuts
16
16
17 from prompt_toolkit.history import InMemoryHistory
17 from prompt_toolkit.history import InMemoryHistory
18 from prompt_toolkit.buffer import Buffer
18 from prompt_toolkit.buffer import Buffer
19 from prompt_toolkit.document import Document
19 from prompt_toolkit.document import Document
20 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
20 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
21
21
22 from unittest.mock import patch, Mock
22 from unittest.mock import patch, Mock
23
23
24
24
25 def test_deprected():
26 import IPython.terminal.shortcuts.auto_suggest as iptsa
27
28 with pytest.warns(DeprecationWarning, match=r"8\.12.+accept_or_jump_to_end"):
29 iptsa.accept_in_vi_insert_mode
30
31
25 def make_event(text, cursor, suggestion):
32 def make_event(text, cursor, suggestion):
26 event = Mock()
33 event = Mock()
27 event.current_buffer = Mock()
34 event.current_buffer = Mock()
28 event.current_buffer.suggestion = Mock()
35 event.current_buffer.suggestion = Mock()
29 event.current_buffer.text = text
36 event.current_buffer.text = text
30 event.current_buffer.cursor_position = cursor
37 event.current_buffer.cursor_position = cursor
31 event.current_buffer.suggestion.text = suggestion
38 event.current_buffer.suggestion.text = suggestion
32 event.current_buffer.document = Document(text=text, cursor_position=cursor)
39 event.current_buffer.document = Document(text=text, cursor_position=cursor)
33 return event
40 return event
34
41
35
42
36 @pytest.mark.parametrize(
43 @pytest.mark.parametrize(
37 "text, suggestion, expected",
44 "text, suggestion, expected",
38 [
45 [
39 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):"),
46 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):"),
40 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):"),
47 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):"),
41 ],
48 ],
42 )
49 )
43 def test_accept(text, suggestion, expected):
50 def test_accept(text, suggestion, expected):
44 event = make_event(text, len(text), suggestion)
51 event = make_event(text, len(text), suggestion)
45 buffer = event.current_buffer
52 buffer = event.current_buffer
46 buffer.insert_text = Mock()
53 buffer.insert_text = Mock()
47 accept(event)
54 accept(event)
48 assert buffer.insert_text.called
55 assert buffer.insert_text.called
49 assert buffer.insert_text.call_args[0] == (expected,)
56 assert buffer.insert_text.call_args[0] == (expected,)
50
57
51
58
52 @pytest.mark.parametrize(
59 @pytest.mark.parametrize(
53 "text, suggestion",
60 "text, suggestion",
54 [
61 [
55 ("", "def out(tag: str, n=50):"),
62 ("", "def out(tag: str, n=50):"),
56 ("def ", "out(tag: str, n=50):"),
63 ("def ", "out(tag: str, n=50):"),
57 ],
64 ],
58 )
65 )
59 def test_discard(text, suggestion):
66 def test_discard(text, suggestion):
60 event = make_event(text, len(text), suggestion)
67 event = make_event(text, len(text), suggestion)
61 buffer = event.current_buffer
68 buffer = event.current_buffer
62 buffer.insert_text = Mock()
69 buffer.insert_text = Mock()
63 discard(event)
70 discard(event)
64 assert not buffer.insert_text.called
71 assert not buffer.insert_text.called
65 assert buffer.suggestion is None
72 assert buffer.suggestion is None
66
73
67
74
68 @pytest.mark.parametrize(
75 @pytest.mark.parametrize(
69 "text, cursor, suggestion, called",
76 "text, cursor, suggestion, called",
70 [
77 [
71 ("123456", 6, "123456789", True),
78 ("123456", 6, "123456789", True),
72 ("123456", 3, "123456789", False),
79 ("123456", 3, "123456789", False),
73 ("123456 \n789", 6, "123456789", True),
80 ("123456 \n789", 6, "123456789", True),
74 ],
81 ],
75 )
82 )
76 def test_autosuggest_at_EOL(text, cursor, suggestion, called):
83 def test_autosuggest_at_EOL(text, cursor, suggestion, called):
77 """
84 """
78 test that autosuggest is only applied at end of line.
85 test that autosuggest is only applied at end of line.
79 """
86 """
80
87
81 event = make_event(text, cursor, suggestion)
88 event = make_event(text, cursor, suggestion)
82 event.current_buffer.insert_text = Mock()
89 event.current_buffer.insert_text = Mock()
83 accept_in_vi_insert_mode(event)
90 accept_or_jump_to_end(event)
84 if called:
91 if called:
85 event.current_buffer.insert_text.assert_called()
92 event.current_buffer.insert_text.assert_called()
86 else:
93 else:
87 event.current_buffer.insert_text.assert_not_called()
94 event.current_buffer.insert_text.assert_not_called()
88 # event.current_buffer.document.get_end_of_line_position.assert_called()
95 # event.current_buffer.document.get_end_of_line_position.assert_called()
89
96
90
97
91 @pytest.mark.parametrize(
98 @pytest.mark.parametrize(
92 "text, suggestion, expected",
99 "text, suggestion, expected",
93 [
100 [
94 ("", "def out(tag: str, n=50):", "def "),
101 ("", "def out(tag: str, n=50):", "def "),
95 ("d", "ef out(tag: str, n=50):", "ef "),
102 ("d", "ef out(tag: str, n=50):", "ef "),
96 ("de ", "f out(tag: str, n=50):", "f "),
103 ("de ", "f out(tag: str, n=50):", "f "),
97 ("def", " out(tag: str, n=50):", " "),
104 ("def", " out(tag: str, n=50):", " "),
98 ("def ", "out(tag: str, n=50):", "out("),
105 ("def ", "out(tag: str, n=50):", "out("),
99 ("def o", "ut(tag: str, n=50):", "ut("),
106 ("def o", "ut(tag: str, n=50):", "ut("),
100 ("def ou", "t(tag: str, n=50):", "t("),
107 ("def ou", "t(tag: str, n=50):", "t("),
101 ("def out", "(tag: str, n=50):", "("),
108 ("def out", "(tag: str, n=50):", "("),
102 ("def out(", "tag: str, n=50):", "tag: "),
109 ("def out(", "tag: str, n=50):", "tag: "),
103 ("def out(t", "ag: str, n=50):", "ag: "),
110 ("def out(t", "ag: str, n=50):", "ag: "),
104 ("def out(ta", "g: str, n=50):", "g: "),
111 ("def out(ta", "g: str, n=50):", "g: "),
105 ("def out(tag", ": str, n=50):", ": "),
112 ("def out(tag", ": str, n=50):", ": "),
106 ("def out(tag:", " str, n=50):", " "),
113 ("def out(tag:", " str, n=50):", " "),
107 ("def out(tag: ", "str, n=50):", "str, "),
114 ("def out(tag: ", "str, n=50):", "str, "),
108 ("def out(tag: s", "tr, n=50):", "tr, "),
115 ("def out(tag: s", "tr, n=50):", "tr, "),
109 ("def out(tag: st", "r, n=50):", "r, "),
116 ("def out(tag: st", "r, n=50):", "r, "),
110 ("def out(tag: str", ", n=50):", ", n"),
117 ("def out(tag: str", ", n=50):", ", n"),
111 ("def out(tag: str,", " n=50):", " n"),
118 ("def out(tag: str,", " n=50):", " n"),
112 ("def out(tag: str, ", "n=50):", "n="),
119 ("def out(tag: str, ", "n=50):", "n="),
113 ("def out(tag: str, n", "=50):", "="),
120 ("def out(tag: str, n", "=50):", "="),
114 ("def out(tag: str, n=", "50):", "50)"),
121 ("def out(tag: str, n=", "50):", "50)"),
115 ("def out(tag: str, n=5", "0):", "0)"),
122 ("def out(tag: str, n=5", "0):", "0)"),
116 ("def out(tag: str, n=50", "):", "):"),
123 ("def out(tag: str, n=50", "):", "):"),
117 ("def out(tag: str, n=50)", ":", ":"),
124 ("def out(tag: str, n=50)", ":", ":"),
118 ],
125 ],
119 )
126 )
120 def test_autosuggest_token(text, suggestion, expected):
127 def test_autosuggest_token(text, suggestion, expected):
121 event = make_event(text, len(text), suggestion)
128 event = make_event(text, len(text), suggestion)
122 event.current_buffer.insert_text = Mock()
129 event.current_buffer.insert_text = Mock()
123 accept_token(event)
130 accept_token(event)
124 assert event.current_buffer.insert_text.called
131 assert event.current_buffer.insert_text.called
125 assert event.current_buffer.insert_text.call_args[0] == (expected,)
132 assert event.current_buffer.insert_text.call_args[0] == (expected,)
126
133
127
134
128 @pytest.mark.parametrize(
135 @pytest.mark.parametrize(
129 "text, suggestion, expected",
136 "text, suggestion, expected",
130 [
137 [
131 ("", "def out(tag: str, n=50):", "d"),
138 ("", "def out(tag: str, n=50):", "d"),
132 ("d", "ef out(tag: str, n=50):", "e"),
139 ("d", "ef out(tag: str, n=50):", "e"),
133 ("de ", "f out(tag: str, n=50):", "f"),
140 ("de ", "f out(tag: str, n=50):", "f"),
134 ("def", " out(tag: str, n=50):", " "),
141 ("def", " out(tag: str, n=50):", " "),
135 ],
142 ],
136 )
143 )
137 def test_accept_character(text, suggestion, expected):
144 def test_accept_character(text, suggestion, expected):
138 event = make_event(text, len(text), suggestion)
145 event = make_event(text, len(text), suggestion)
139 event.current_buffer.insert_text = Mock()
146 event.current_buffer.insert_text = Mock()
140 accept_character(event)
147 accept_character(event)
141 assert event.current_buffer.insert_text.called
148 assert event.current_buffer.insert_text.called
142 assert event.current_buffer.insert_text.call_args[0] == (expected,)
149 assert event.current_buffer.insert_text.call_args[0] == (expected,)
143
150
144
151
145 @pytest.mark.parametrize(
152 @pytest.mark.parametrize(
146 "text, suggestion, expected",
153 "text, suggestion, expected",
147 [
154 [
148 ("", "def out(tag: str, n=50):", "def "),
155 ("", "def out(tag: str, n=50):", "def "),
149 ("d", "ef out(tag: str, n=50):", "ef "),
156 ("d", "ef out(tag: str, n=50):", "ef "),
150 ("de", "f out(tag: str, n=50):", "f "),
157 ("de", "f out(tag: str, n=50):", "f "),
151 ("def", " out(tag: str, n=50):", " "),
158 ("def", " out(tag: str, n=50):", " "),
152 # (this is why we also have accept_token)
159 # (this is why we also have accept_token)
153 ("def ", "out(tag: str, n=50):", "out(tag: "),
160 ("def ", "out(tag: str, n=50):", "out(tag: "),
154 ],
161 ],
155 )
162 )
156 def test_accept_word(text, suggestion, expected):
163 def test_accept_word(text, suggestion, expected):
157 event = make_event(text, len(text), suggestion)
164 event = make_event(text, len(text), suggestion)
158 event.current_buffer.insert_text = Mock()
165 event.current_buffer.insert_text = Mock()
159 accept_word(event)
166 accept_word(event)
160 assert event.current_buffer.insert_text.called
167 assert event.current_buffer.insert_text.called
161 assert event.current_buffer.insert_text.call_args[0] == (expected,)
168 assert event.current_buffer.insert_text.call_args[0] == (expected,)
162
169
163
170
164 @pytest.mark.parametrize(
171 @pytest.mark.parametrize(
165 "text, suggestion, expected, cursor",
172 "text, suggestion, expected, cursor",
166 [
173 [
167 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):", 0),
174 ("", "def out(tag: str, n=50):", "def out(tag: str, n=50):", 0),
168 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):", 4),
175 ("def ", "out(tag: str, n=50):", "out(tag: str, n=50):", 4),
169 ],
176 ],
170 )
177 )
171 def test_accept_and_keep_cursor(text, suggestion, expected, cursor):
178 def test_accept_and_keep_cursor(text, suggestion, expected, cursor):
172 event = make_event(text, cursor, suggestion)
179 event = make_event(text, cursor, suggestion)
173 buffer = event.current_buffer
180 buffer = event.current_buffer
174 buffer.insert_text = Mock()
181 buffer.insert_text = Mock()
175 accept_and_keep_cursor(event)
182 accept_and_keep_cursor(event)
176 assert buffer.insert_text.called
183 assert buffer.insert_text.called
177 assert buffer.insert_text.call_args[0] == (expected,)
184 assert buffer.insert_text.call_args[0] == (expected,)
178 assert buffer.cursor_position == cursor
185 assert buffer.cursor_position == cursor
179
186
180
187
181 def test_autosuggest_token_empty():
188 def test_autosuggest_token_empty():
182 full = "def out(tag: str, n=50):"
189 full = "def out(tag: str, n=50):"
183 event = make_event(full, len(full), "")
190 event = make_event(full, len(full), "")
184 event.current_buffer.insert_text = Mock()
191 event.current_buffer.insert_text = Mock()
185
192
186 with patch(
193 with patch(
187 "prompt_toolkit.key_binding.bindings.named_commands.forward_word"
194 "prompt_toolkit.key_binding.bindings.named_commands.forward_word"
188 ) as forward_word:
195 ) as forward_word:
189 accept_token(event)
196 accept_token(event)
190 assert not event.current_buffer.insert_text.called
197 assert not event.current_buffer.insert_text.called
191 assert forward_word.called
198 assert forward_word.called
192
199
193
200
194 def test_other_providers():
201 def test_other_providers():
195 """Ensure that swapping autosuggestions does not break with other providers"""
202 """Ensure that swapping autosuggestions does not break with other providers"""
196 provider = AutoSuggestFromHistory()
203 provider = AutoSuggestFromHistory()
197 ip = get_ipython()
204 ip = get_ipython()
198 ip.auto_suggest = provider
205 ip.auto_suggest = provider
199 event = Mock()
206 event = Mock()
200 event.current_buffer = Buffer()
207 event.current_buffer = Buffer()
201 assert swap_autosuggestion_up(event) is None
208 assert swap_autosuggestion_up(event) is None
202 assert swap_autosuggestion_down(event) is None
209 assert swap_autosuggestion_down(event) is None
203
210
204
211
205 async def test_navigable_provider():
212 async def test_navigable_provider():
206 provider = NavigableAutoSuggestFromHistory()
213 provider = NavigableAutoSuggestFromHistory()
207 history = InMemoryHistory(history_strings=["very_a", "very", "very_b", "very_c"])
214 history = InMemoryHistory(history_strings=["very_a", "very", "very_b", "very_c"])
208 buffer = Buffer(history=history)
215 buffer = Buffer(history=history)
209 ip = get_ipython()
216 ip = get_ipython()
210 ip.auto_suggest = provider
217 ip.auto_suggest = provider
211
218
212 async for _ in history.load():
219 async for _ in history.load():
213 pass
220 pass
214
221
215 buffer.cursor_position = 5
222 buffer.cursor_position = 5
216 buffer.text = "very"
223 buffer.text = "very"
217
224
218 up = swap_autosuggestion_up
225 up = swap_autosuggestion_up
219 down = swap_autosuggestion_down
226 down = swap_autosuggestion_down
220
227
221 event = Mock()
228 event = Mock()
222 event.current_buffer = buffer
229 event.current_buffer = buffer
223
230
224 def get_suggestion():
231 def get_suggestion():
225 suggestion = provider.get_suggestion(buffer, buffer.document)
232 suggestion = provider.get_suggestion(buffer, buffer.document)
226 buffer.suggestion = suggestion
233 buffer.suggestion = suggestion
227 return suggestion
234 return suggestion
228
235
229 assert get_suggestion().text == "_c"
236 assert get_suggestion().text == "_c"
230
237
231 # should go up
238 # should go up
232 up(event)
239 up(event)
233 assert get_suggestion().text == "_b"
240 assert get_suggestion().text == "_b"
234
241
235 # should skip over 'very' which is identical to buffer content
242 # should skip over 'very' which is identical to buffer content
236 up(event)
243 up(event)
237 assert get_suggestion().text == "_a"
244 assert get_suggestion().text == "_a"
238
245
239 # should cycle back to beginning
246 # should cycle back to beginning
240 up(event)
247 up(event)
241 assert get_suggestion().text == "_c"
248 assert get_suggestion().text == "_c"
242
249
243 # should cycle back through end boundary
250 # should cycle back through end boundary
244 down(event)
251 down(event)
245 assert get_suggestion().text == "_a"
252 assert get_suggestion().text == "_a"
246
253
247 down(event)
254 down(event)
248 assert get_suggestion().text == "_b"
255 assert get_suggestion().text == "_b"
249
256
250 down(event)
257 down(event)
251 assert get_suggestion().text == "_c"
258 assert get_suggestion().text == "_c"
252
259
253 down(event)
260 down(event)
254 assert get_suggestion().text == "_a"
261 assert get_suggestion().text == "_a"
255
262
256
263
257 async def test_navigable_provider_multiline_entries():
264 async def test_navigable_provider_multiline_entries():
258 provider = NavigableAutoSuggestFromHistory()
265 provider = NavigableAutoSuggestFromHistory()
259 history = InMemoryHistory(history_strings=["very_a\nvery_b", "very_c"])
266 history = InMemoryHistory(history_strings=["very_a\nvery_b", "very_c"])
260 buffer = Buffer(history=history)
267 buffer = Buffer(history=history)
261 ip = get_ipython()
268 ip = get_ipython()
262 ip.auto_suggest = provider
269 ip.auto_suggest = provider
263
270
264 async for _ in history.load():
271 async for _ in history.load():
265 pass
272 pass
266
273
267 buffer.cursor_position = 5
274 buffer.cursor_position = 5
268 buffer.text = "very"
275 buffer.text = "very"
269 up = swap_autosuggestion_up
276 up = swap_autosuggestion_up
270 down = swap_autosuggestion_down
277 down = swap_autosuggestion_down
271
278
272 event = Mock()
279 event = Mock()
273 event.current_buffer = buffer
280 event.current_buffer = buffer
274
281
275 def get_suggestion():
282 def get_suggestion():
276 suggestion = provider.get_suggestion(buffer, buffer.document)
283 suggestion = provider.get_suggestion(buffer, buffer.document)
277 buffer.suggestion = suggestion
284 buffer.suggestion = suggestion
278 return suggestion
285 return suggestion
279
286
280 assert get_suggestion().text == "_c"
287 assert get_suggestion().text == "_c"
281
288
282 up(event)
289 up(event)
283 assert get_suggestion().text == "_b"
290 assert get_suggestion().text == "_b"
284
291
285 up(event)
292 up(event)
286 assert get_suggestion().text == "_a"
293 assert get_suggestion().text == "_a"
287
294
288 down(event)
295 down(event)
289 assert get_suggestion().text == "_b"
296 assert get_suggestion().text == "_b"
290
297
291 down(event)
298 down(event)
292 assert get_suggestion().text == "_c"
299 assert get_suggestion().text == "_c"
293
300
294
301
295 def create_session_mock():
302 def create_session_mock():
296 session = Mock()
303 session = Mock()
297 session.default_buffer = Buffer()
304 session.default_buffer = Buffer()
298 return session
305 return session
299
306
300
307
301 def test_navigable_provider_connection():
308 def test_navigable_provider_connection():
302 provider = NavigableAutoSuggestFromHistory()
309 provider = NavigableAutoSuggestFromHistory()
303 provider.skip_lines = 1
310 provider.skip_lines = 1
304
311
305 session_1 = create_session_mock()
312 session_1 = create_session_mock()
306 provider.connect(session_1)
313 provider.connect(session_1)
307
314
308 assert provider.skip_lines == 1
315 assert provider.skip_lines == 1
309 session_1.default_buffer.on_text_insert.fire()
316 session_1.default_buffer.on_text_insert.fire()
310 assert provider.skip_lines == 0
317 assert provider.skip_lines == 0
311
318
312 session_2 = create_session_mock()
319 session_2 = create_session_mock()
313 provider.connect(session_2)
320 provider.connect(session_2)
314 provider.skip_lines = 2
321 provider.skip_lines = 2
315
322
316 assert provider.skip_lines == 2
323 assert provider.skip_lines == 2
317 session_2.default_buffer.on_text_insert.fire()
324 session_2.default_buffer.on_text_insert.fire()
318 assert provider.skip_lines == 0
325 assert provider.skip_lines == 0
319
326
320 provider.skip_lines = 3
327 provider.skip_lines = 3
321 provider.disconnect()
328 provider.disconnect()
322 session_1.default_buffer.on_text_insert.fire()
329 session_1.default_buffer.on_text_insert.fire()
323 session_2.default_buffer.on_text_insert.fire()
330 session_2.default_buffer.on_text_insert.fire()
324 assert provider.skip_lines == 3
331 assert provider.skip_lines == 3
325
332
326
333
327 @pytest.fixture
334 @pytest.fixture
328 def ipython_with_prompt():
335 def ipython_with_prompt():
329 ip = get_ipython()
336 ip = get_ipython()
330 ip.pt_app = Mock()
337 ip.pt_app = Mock()
331 ip.pt_app.key_bindings = create_ipython_shortcuts(ip)
338 ip.pt_app.key_bindings = create_ipython_shortcuts(ip)
332 try:
339 try:
333 yield ip
340 yield ip
334 finally:
341 finally:
335 ip.pt_app = None
342 ip.pt_app = None
336
343
337
344
338 def find_bindings_by_command(command):
345 def find_bindings_by_command(command):
339 ip = get_ipython()
346 ip = get_ipython()
340 return [
347 return [
341 binding
348 binding
342 for binding in ip.pt_app.key_bindings.bindings
349 for binding in ip.pt_app.key_bindings.bindings
343 if binding.handler == command
350 if binding.handler == command
344 ]
351 ]
345
352
346
353
347 def test_modify_unique_shortcut(ipython_with_prompt):
354 def test_modify_unique_shortcut(ipython_with_prompt):
348 original = find_bindings_by_command(accept_token)
355 original = find_bindings_by_command(accept_token)
349 assert len(original) == 1
356 assert len(original) == 1
350
357
351 ipython_with_prompt.shortcuts = [
358 ipython_with_prompt.shortcuts = [
352 {"command": "IPython:auto_suggest.accept_token", "new_keys": ["a", "b", "c"]}
359 {"command": "IPython:auto_suggest.accept_token", "new_keys": ["a", "b", "c"]}
353 ]
360 ]
354 matched = find_bindings_by_command(accept_token)
361 matched = find_bindings_by_command(accept_token)
355 assert len(matched) == 1
362 assert len(matched) == 1
356 assert list(matched[0].keys) == ["a", "b", "c"]
363 assert list(matched[0].keys) == ["a", "b", "c"]
357 assert list(matched[0].keys) != list(original[0].keys)
364 assert list(matched[0].keys) != list(original[0].keys)
358 assert matched[0].filter == original[0].filter
365 assert matched[0].filter == original[0].filter
359
366
360 ipython_with_prompt.shortcuts = [
367 ipython_with_prompt.shortcuts = [
361 {"command": "IPython:auto_suggest.accept_token", "new_filter": "always"}
368 {"command": "IPython:auto_suggest.accept_token", "new_filter": "always"}
362 ]
369 ]
363 matched = find_bindings_by_command(accept_token)
370 matched = find_bindings_by_command(accept_token)
364 assert len(matched) == 1
371 assert len(matched) == 1
365 assert list(matched[0].keys) != ["a", "b", "c"]
372 assert list(matched[0].keys) != ["a", "b", "c"]
366 assert list(matched[0].keys) == list(original[0].keys)
373 assert list(matched[0].keys) == list(original[0].keys)
367 assert matched[0].filter != original[0].filter
374 assert matched[0].filter != original[0].filter
368
375
369
376
370 def test_disable_shortcut(ipython_with_prompt):
377 def test_disable_shortcut(ipython_with_prompt):
371 matched = find_bindings_by_command(accept_token)
378 matched = find_bindings_by_command(accept_token)
372 assert len(matched) == 1
379 assert len(matched) == 1
373
380
374 ipython_with_prompt.shortcuts = [
381 ipython_with_prompt.shortcuts = [
375 {"command": "IPython:auto_suggest.accept_token", "new_keys": []}
382 {"command": "IPython:auto_suggest.accept_token", "new_keys": []}
376 ]
383 ]
377 matched = find_bindings_by_command(accept_token)
384 matched = find_bindings_by_command(accept_token)
378 assert len(matched) == 0
385 assert len(matched) == 0
379
386
380 ipython_with_prompt.shortcuts = []
387 ipython_with_prompt.shortcuts = []
381 matched = find_bindings_by_command(accept_token)
388 matched = find_bindings_by_command(accept_token)
382 assert len(matched) == 1
389 assert len(matched) == 1
383
390
384
391
385 def test_modify_shortcut_with_filters(ipython_with_prompt):
392 def test_modify_shortcut_with_filters(ipython_with_prompt):
386 matched = find_bindings_by_command(skip_over)
393 matched = find_bindings_by_command(skip_over)
387 matched_keys = {m.keys[0] for m in matched}
394 matched_keys = {m.keys[0] for m in matched}
388 assert matched_keys == {")", "]", "}", "'", '"'}
395 assert matched_keys == {")", "]", "}", "'", '"'}
389
396
390 with pytest.raises(ValueError, match="Multiple shortcuts matching"):
397 with pytest.raises(ValueError, match="Multiple shortcuts matching"):
391 ipython_with_prompt.shortcuts = [
398 ipython_with_prompt.shortcuts = [
392 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"]}
399 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"]}
393 ]
400 ]
394
401
395 ipython_with_prompt.shortcuts = [
402 ipython_with_prompt.shortcuts = [
396 {
403 {
397 "command": "IPython:auto_match.skip_over",
404 "command": "IPython:auto_match.skip_over",
398 "new_keys": ["x"],
405 "new_keys": ["x"],
399 "match_filter": "focused_insert & auto_match & followed_by_single_quote",
406 "match_filter": "focused_insert & auto_match & followed_by_single_quote",
400 }
407 }
401 ]
408 ]
402 matched = find_bindings_by_command(skip_over)
409 matched = find_bindings_by_command(skip_over)
403 matched_keys = {m.keys[0] for m in matched}
410 matched_keys = {m.keys[0] for m in matched}
404 assert matched_keys == {")", "]", "}", "x", '"'}
411 assert matched_keys == {")", "]", "}", "x", '"'}
405
412
406
413
407 def example_command():
414 def example_command():
408 pass
415 pass
409
416
410
417
411 def test_add_shortcut_for_new_command(ipython_with_prompt):
418 def test_add_shortcut_for_new_command(ipython_with_prompt):
412 matched = find_bindings_by_command(example_command)
419 matched = find_bindings_by_command(example_command)
413 assert len(matched) == 0
420 assert len(matched) == 0
414
421
415 with pytest.raises(ValueError, match="example_command is not a known"):
422 with pytest.raises(ValueError, match="example_command is not a known"):
416 ipython_with_prompt.shortcuts = [
423 ipython_with_prompt.shortcuts = [
417 {"command": "example_command", "new_keys": ["x"]}
424 {"command": "example_command", "new_keys": ["x"]}
418 ]
425 ]
419 matched = find_bindings_by_command(example_command)
426 matched = find_bindings_by_command(example_command)
420 assert len(matched) == 0
427 assert len(matched) == 0
421
428
422
429
423 def test_modify_shortcut_failure(ipython_with_prompt):
430 def test_modify_shortcut_failure(ipython_with_prompt):
424 with pytest.raises(ValueError, match="No shortcuts matching"):
431 with pytest.raises(ValueError, match="No shortcuts matching"):
425 ipython_with_prompt.shortcuts = [
432 ipython_with_prompt.shortcuts = [
426 {
433 {
427 "command": "IPython:auto_match.skip_over",
434 "command": "IPython:auto_match.skip_over",
428 "match_keys": ["x"],
435 "match_keys": ["x"],
429 "new_keys": ["y"],
436 "new_keys": ["y"],
430 }
437 }
431 ]
438 ]
432
439
433
440
434 def test_add_shortcut_for_existing_command(ipython_with_prompt):
441 def test_add_shortcut_for_existing_command(ipython_with_prompt):
435 matched = find_bindings_by_command(skip_over)
442 matched = find_bindings_by_command(skip_over)
436 assert len(matched) == 5
443 assert len(matched) == 5
437
444
438 with pytest.raises(ValueError, match="Cannot add a shortcut without keys"):
445 with pytest.raises(ValueError, match="Cannot add a shortcut without keys"):
439 ipython_with_prompt.shortcuts = [
446 ipython_with_prompt.shortcuts = [
440 {"command": "IPython:auto_match.skip_over", "new_keys": [], "create": True}
447 {"command": "IPython:auto_match.skip_over", "new_keys": [], "create": True}
441 ]
448 ]
442
449
443 ipython_with_prompt.shortcuts = [
450 ipython_with_prompt.shortcuts = [
444 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"], "create": True}
451 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"], "create": True}
445 ]
452 ]
446 matched = find_bindings_by_command(skip_over)
453 matched = find_bindings_by_command(skip_over)
447 assert len(matched) == 6
454 assert len(matched) == 6
448
455
449 ipython_with_prompt.shortcuts = []
456 ipython_with_prompt.shortcuts = []
450 matched = find_bindings_by_command(skip_over)
457 matched = find_bindings_by_command(skip_over)
451 assert len(matched) == 5
458 assert len(matched) == 5
452
459
453
460
454 def test_setting_shortcuts_before_pt_app_init():
461 def test_setting_shortcuts_before_pt_app_init():
455 ipython = get_ipython()
462 ipython = get_ipython()
456 assert ipython.pt_app is None
463 assert ipython.pt_app is None
457 shortcuts = [
464 shortcuts = [
458 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"], "create": True}
465 {"command": "IPython:auto_match.skip_over", "new_keys": ["x"], "create": True}
459 ]
466 ]
460 ipython.shortcuts = shortcuts
467 ipython.shortcuts = shortcuts
461 assert ipython.shortcuts == shortcuts
468 assert ipython.shortcuts == shortcuts
General Comments 0
You need to be logged in to leave comments. Login now