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