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