Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/IPython/terminal/shortcuts/auto_suggest.py: 24%
195 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:07 +0000
« prev ^ index » next coverage.py v7.2.2, created at 2023-03-26 06:07 +0000
1import re
2import tokenize
3from io import StringIO
4from typing import Callable, List, Optional, Union, Generator, Tuple
6from prompt_toolkit.buffer import Buffer
7from prompt_toolkit.key_binding import KeyPressEvent
8from prompt_toolkit.key_binding.bindings import named_commands as nc
9from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
10from prompt_toolkit.document import Document
11from prompt_toolkit.history import History
12from prompt_toolkit.shortcuts import PromptSession
13from prompt_toolkit.layout.processors import (
14 Processor,
15 Transformation,
16 TransformationInput,
17)
19from IPython.core.getipython import get_ipython
20from IPython.utils.tokenutil import generate_tokens
23def _get_query(document: Document):
24 return document.lines[document.cursor_position_row]
27class AppendAutoSuggestionInAnyLine(Processor):
28 """
29 Append the auto suggestion to lines other than the last (appending to the
30 last line is natively supported by the prompt toolkit).
31 """
33 def __init__(self, style: str = "class:auto-suggestion") -> None:
34 self.style = style
36 def apply_transformation(self, ti: TransformationInput) -> Transformation:
37 is_last_line = ti.lineno == ti.document.line_count - 1
38 is_active_line = ti.lineno == ti.document.cursor_position_row
40 if not is_last_line and is_active_line:
41 buffer = ti.buffer_control.buffer
43 if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
44 suggestion = buffer.suggestion.text
45 else:
46 suggestion = ""
48 return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
49 else:
50 return Transformation(fragments=ti.fragments)
53class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
54 """
55 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
56 suggestion from history. To do so it remembers the current position, but it
57 state need to carefully be cleared on the right events.
58 """
60 def __init__(
61 self,
62 ):
63 self.skip_lines = 0
64 self._connected_apps = []
66 def reset_history_position(self, _: Buffer):
67 self.skip_lines = 0
69 def disconnect(self):
70 for pt_app in self._connected_apps:
71 text_insert_event = pt_app.default_buffer.on_text_insert
72 text_insert_event.remove_handler(self.reset_history_position)
74 def connect(self, pt_app: PromptSession):
75 self._connected_apps.append(pt_app)
76 # note: `on_text_changed` could be used for a bit different behaviour
77 # on character deletion (i.e. reseting history position on backspace)
78 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
79 pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
81 def get_suggestion(
82 self, buffer: Buffer, document: Document
83 ) -> Optional[Suggestion]:
84 text = _get_query(document)
86 if text.strip():
87 for suggestion, _ in self._find_next_match(
88 text, self.skip_lines, buffer.history
89 ):
90 return Suggestion(suggestion)
92 return None
94 def _dismiss(self, buffer, *args, **kwargs):
95 buffer.suggestion = None
97 def _find_match(
98 self, text: str, skip_lines: float, history: History, previous: bool
99 ) -> Generator[Tuple[str, float], None, None]:
100 """
101 text : str
102 Text content to find a match for, the user cursor is most of the
103 time at the end of this text.
104 skip_lines : float
105 number of items to skip in the search, this is used to indicate how
106 far in the list the user has navigated by pressing up or down.
107 The float type is used as the base value is +inf
108 history : History
109 prompt_toolkit History instance to fetch previous entries from.
110 previous : bool
111 Direction of the search, whether we are looking previous match
112 (True), or next match (False).
114 Yields
115 ------
116 Tuple with:
117 str:
118 current suggestion.
119 float:
120 will actually yield only ints, which is passed back via skip_lines,
121 which may be a +inf (float)
124 """
125 line_number = -1
126 for string in reversed(list(history.get_strings())):
127 for line in reversed(string.splitlines()):
128 line_number += 1
129 if not previous and line_number < skip_lines:
130 continue
131 # do not return empty suggestions as these
132 # close the auto-suggestion overlay (and are useless)
133 if line.startswith(text) and len(line) > len(text):
134 yield line[len(text) :], line_number
135 if previous and line_number >= skip_lines:
136 return
138 def _find_next_match(
139 self, text: str, skip_lines: float, history: History
140 ) -> Generator[Tuple[str, float], None, None]:
141 return self._find_match(text, skip_lines, history, previous=False)
143 def _find_previous_match(self, text: str, skip_lines: float, history: History):
144 return reversed(
145 list(self._find_match(text, skip_lines, history, previous=True))
146 )
148 def up(self, query: str, other_than: str, history: History) -> None:
149 for suggestion, line_number in self._find_next_match(
150 query, self.skip_lines, history
151 ):
152 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
153 # we want to switch from 'very.b' to 'very.a' because a) if the
154 # suggestion equals current text, prompt-toolkit aborts suggesting
155 # b) user likely would not be interested in 'very' anyways (they
156 # already typed it).
157 if query + suggestion != other_than:
158 self.skip_lines = line_number
159 break
160 else:
161 # no matches found, cycle back to beginning
162 self.skip_lines = 0
164 def down(self, query: str, other_than: str, history: History) -> None:
165 for suggestion, line_number in self._find_previous_match(
166 query, self.skip_lines, history
167 ):
168 if query + suggestion != other_than:
169 self.skip_lines = line_number
170 break
171 else:
172 # no matches found, cycle to end
173 for suggestion, line_number in self._find_previous_match(
174 query, float("Inf"), history
175 ):
176 if query + suggestion != other_than:
177 self.skip_lines = line_number
178 break
181# Needed for to accept autosuggestions in vi insert mode
182def accept_in_vi_insert_mode(event: KeyPressEvent):
183 """Apply autosuggestion if at end of line."""
184 buffer = event.current_buffer
185 d = buffer.document
186 after_cursor = d.text[d.cursor_position :]
187 lines = after_cursor.split("\n")
188 end_of_current_line = lines[0].strip()
189 suggestion = buffer.suggestion
190 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
191 buffer.insert_text(suggestion.text)
192 else:
193 nc.end_of_line(event)
196def accept(event: KeyPressEvent):
197 """Accept autosuggestion"""
198 buffer = event.current_buffer
199 suggestion = buffer.suggestion
200 if suggestion:
201 buffer.insert_text(suggestion.text)
202 else:
203 nc.forward_char(event)
206def discard(event: KeyPressEvent):
207 """Discard autosuggestion"""
208 buffer = event.current_buffer
209 buffer.suggestion = None
212def accept_word(event: KeyPressEvent):
213 """Fill partial autosuggestion by word"""
214 buffer = event.current_buffer
215 suggestion = buffer.suggestion
216 if suggestion:
217 t = re.split(r"(\S+\s+)", suggestion.text)
218 buffer.insert_text(next((x for x in t if x), ""))
219 else:
220 nc.forward_word(event)
223def accept_character(event: KeyPressEvent):
224 """Fill partial autosuggestion by character"""
225 b = event.current_buffer
226 suggestion = b.suggestion
227 if suggestion and suggestion.text:
228 b.insert_text(suggestion.text[0])
231def accept_and_keep_cursor(event: KeyPressEvent):
232 """Accept autosuggestion and keep cursor in place"""
233 buffer = event.current_buffer
234 old_position = buffer.cursor_position
235 suggestion = buffer.suggestion
236 if suggestion:
237 buffer.insert_text(suggestion.text)
238 buffer.cursor_position = old_position
241def accept_and_move_cursor_left(event: KeyPressEvent):
242 """Accept autosuggestion and move cursor left in place"""
243 accept_and_keep_cursor(event)
244 nc.backward_char(event)
247def _update_hint(buffer: Buffer):
248 if buffer.auto_suggest:
249 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
250 buffer.suggestion = suggestion
253def backspace_and_resume_hint(event: KeyPressEvent):
254 """Resume autosuggestions after deleting last character"""
255 current_buffer = event.current_buffer
257 def resume_hinting(buffer: Buffer):
258 _update_hint(buffer)
259 current_buffer.on_text_changed.remove_handler(resume_hinting)
261 current_buffer.on_text_changed.add_handler(resume_hinting)
262 nc.backward_delete_char(event)
265def up_and_update_hint(event: KeyPressEvent):
266 """Go up and update hint"""
267 current_buffer = event.current_buffer
269 current_buffer.auto_up(count=event.arg)
270 _update_hint(current_buffer)
273def down_and_update_hint(event: KeyPressEvent):
274 """Go down and update hint"""
275 current_buffer = event.current_buffer
277 current_buffer.auto_down(count=event.arg)
278 _update_hint(current_buffer)
281def accept_token(event: KeyPressEvent):
282 """Fill partial autosuggestion by token"""
283 b = event.current_buffer
284 suggestion = b.suggestion
286 if suggestion:
287 prefix = _get_query(b.document)
288 text = prefix + suggestion.text
290 tokens: List[Optional[str]] = [None, None, None]
291 substrings = [""]
292 i = 0
294 for token in generate_tokens(StringIO(text).readline):
295 if token.type == tokenize.NEWLINE:
296 index = len(text)
297 else:
298 index = text.index(token[1], len(substrings[-1]))
299 substrings.append(text[:index])
300 tokenized_so_far = substrings[-1]
301 if tokenized_so_far.startswith(prefix):
302 if i == 0 and len(tokenized_so_far) > len(prefix):
303 tokens[0] = tokenized_so_far[len(prefix) :]
304 substrings.append(tokenized_so_far)
305 i += 1
306 tokens[i] = token[1]
307 if i == 2:
308 break
309 i += 1
311 if tokens[0]:
312 to_insert: str
313 insert_text = substrings[-2]
314 if tokens[1] and len(tokens[1]) == 1:
315 insert_text = substrings[-1]
316 to_insert = insert_text[len(prefix) :]
317 b.insert_text(to_insert)
318 return
320 nc.forward_word(event)
323Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
326def _swap_autosuggestion(
327 buffer: Buffer,
328 provider: NavigableAutoSuggestFromHistory,
329 direction_method: Callable,
330):
331 """
332 We skip most recent history entry (in either direction) if it equals the
333 current autosuggestion because if user cycles when auto-suggestion is shown
334 they most likely want something else than what was suggested (otherwise
335 they would have accepted the suggestion).
336 """
337 suggestion = buffer.suggestion
338 if not suggestion:
339 return
341 query = _get_query(buffer.document)
342 current = query + suggestion.text
344 direction_method(query=query, other_than=current, history=buffer.history)
346 new_suggestion = provider.get_suggestion(buffer, buffer.document)
347 buffer.suggestion = new_suggestion
350def swap_autosuggestion_up(event: KeyPressEvent):
351 """Get next autosuggestion from history."""
352 shell = get_ipython()
353 provider = shell.auto_suggest
355 if not isinstance(provider, NavigableAutoSuggestFromHistory):
356 return
358 return _swap_autosuggestion(
359 buffer=event.current_buffer, provider=provider, direction_method=provider.up
360 )
363def swap_autosuggestion_down(event: KeyPressEvent):
364 """Get previous autosuggestion from history."""
365 shell = get_ipython()
366 provider = shell.auto_suggest
368 if not isinstance(provider, NavigableAutoSuggestFromHistory):
369 return
371 return _swap_autosuggestion(
372 buffer=event.current_buffer,
373 provider=provider,
374 direction_method=provider.down,
375 )